Skip to content

Add fully functional standalone CLI (no Calibre required)#977

Open
davidxue1989 wants to merge 4 commits intonoDRM:masterfrom
davidxue1989:standalone-cli
Open

Add fully functional standalone CLI (no Calibre required)#977
davidxue1989 wants to merge 4 commits intonoDRM:masterfrom
davidxue1989:standalone-cli

Conversation

@davidxue1989
Copy link
Copy Markdown

Summary

Implements a complete standalone CLI so users can remove DRM without installing Calibre. The DRM-removal logic already existed in the individual format modules — this PR adds the missing glue, a self-bootstrapping entry point, and documentation.

What was missing / broken

  • standalone/remove_drm.py was a non-functional stub (upstream comment: "there's only a rough code structure and no working code yet")
  • standalone/__init__.py silently dropped perform_action's return value, so the process always exited 0 even on decryption failure
  • No entry point existed for running the plugin outside Calibre without manually building the zip first

Changes

Bug fixes

  • standalone/__init__.pyexecute_action now propagates perform_action's exit code via sys.exit(). Failure now correctly returns exit code 1.

Core implementation

  • standalone/remove_drm.py — full implementation of dedrm_single_file() and helpers, covering all supported formats:
    • Adobe ADEPT ePub (UUID-matched key tried first, then all stored keys, then auto-discovery)
    • B&N PassHash ePub (stored keys → NOOK Study → NOOK Store → ADE PassHash auto-discovery)
    • Adobe ADEPT / Standard PDF
    • Kindle / Mobipocket / Topaz / KFX (stored keys → auto-discovery)
    • eReader PDB
    • DRM-free ePub passthrough
    • ePub font deobfuscation post-processing
    • Also fixes a loop bug in the upstream Calibre plugin's PassHash key-try logic (returns inside loop after first attempt regardless of success)

Compat shim

  • __calibre_compat_code.py — added _DeDRMFinder meta path hook for standalone relative-import support (Python 3.12+ compatible). The Calibre branch is unchanged.

Minor fix

  • prefs.pyfrom __init__ importfrom __version__ import to avoid import ambiguity when running as a zipapp.

New files

  • dedrm.py — single entry point for agents and users: auto-installs deps (lxml, pycryptodome), builds DeDRM_plugin.zip from source if missing or stale, delegates all args to the plugin CLI unchanged
  • build_plugin.py — fast zip builder for development iteration (~65 lines)
  • requirements.txt — explicit dependency list with floor versions
  • STANDALONE_CLI.md — user and agent documentation
  • dedrm_revision_plan.md — developer design reference

Design: agent-skill transparency

DeDRM_plugin.zip is not committed. It is .gitignored and rebuilt on demand from source. This ensures any AI agent pulling this repo as a skill can audit every line before execution:

clone → read dedrm.py + build_plugin.py + DeDRM_plugin/*.py → trust → run

No binary blobs. No network calls at runtime. No eval/exec of downloaded code.

Calibre plugin compatibility

All changes are additive or in standalone-only branches. The Calibre plugin path is unaffected. make_release.py still produces a valid DeDRM_plugin.zip for Calibre users.

Tested

  • Adobe ADEPT ePub (Unwanted.epub via Adobe Digital Editions): decrypted in ~0.8s via UUID-matched ADE key auto-discovery on Windows
  • Exit codes verified: 0 on success, 1 on failure

🤖 Generated with Claude Code

davidxue1989 and others added 4 commits March 20, 2026 11:33
Implements a fully functional standalone command-line interface that removes
DRM from ebooks (Adobe ADEPT, Kindle, B&N, eReader, PDF) without requiring
Calibre to be installed.

## Entry point

`dedrm.py` is the single command for agents and users:

  python3 dedrm.py remove_drm book.epub -o book_nodrm.epub

It auto-installs Python dependencies (lxml, pycryptodome), builds
DeDRM_plugin.zip from source on first run, detects source staleness and
rebuilds when any source file changes, then delegates all arguments to the
plugin CLI. Exit codes: 0 = success, 1 = decryption failure.

## Build pipeline

`build_plugin.py` produces DeDRM_plugin.zip by copying DeDRM_plugin/ source
and inlining the calibre compat shim (replacing #@@CALIBRE_COMPAT_CODE@@
placeholders). The zip is a local build cache — gitignored, never committed,
rebuilt automatically by dedrm.py when stale.

## Design decisions

**Source is the truth, not the zip.** Committing a binary zip would be opaque
to security audits and could drift from source. Agents that use this as a
skill can read all .py files before trusting and running the tool. The build
step is a transparent, deterministic transformation of that same source.

**Compat code extended for Python 3.12+ standalone use.** The existing
__calibre_compat_code.py shim already handled the Calibre case by setting
__package__ = "calibre_plugins.dedrm". An else-branch adds a _DeDRMFinder
meta_path hook (using find_spec, required in Python 3.12+) that maps
`import dedrm.X` to `import X`, enabling relative imports in ineptepub.py
and other modules when running from a flat zip without Calibre's package
context. Calibre behavior is completely unchanged.

**prefs.py import fix.** `from __init__ import PLUGIN_NAME` is ambiguous in
the flat-zip context because the calibre compat code adds
`DeDRM_plugin.zip/standalone` to sys.path (to support Calibre < 5), which
shadows the root __init__.py. Fixed with a try/except that prefers
`from __version__ import PLUGIN_NAME` and falls back for compatibility.

**Bug fix: PassHash key loop.** The original Calibre ePubDecrypt() returned
the still-encrypted file inside the auto-discovered-key loop after the first
attempt regardless of success. The standalone implementation tries all keys
before reporting failure.

**Plain KFX detection.** Added `\xeaDRMION\xee` magic-byte detection for
KFX files not wrapped in a ZIP container (plain .kfx), routing them to the
same k4mobidedrm branch as KFX-ZIP.

**No DeACSM integration.** The Calibre plugin's checkForDeACSMkeys() imports
from calibre_plugins.deacsm — impossible outside Calibre. Omitted with a
documented workaround: export the key from DeACSM manually and add to
dedrm.json.

**Watermark removal omitted.** epubwatermark.py uses the Calibre plugin
object (self.temporary_file etc.) throughout. Out of scope for standalone.

**Failure semantics.** Calibre returns the encrypted file on failure so
Calibre can handle it gracefully. The standalone CLI returns exit code 1 and
writes no output file — correct for a command-line tool.

## Key storage

Decryption keys are auto-discovered from installed ADE / Kindle / NOOK apps
on Windows and macOS. Successfully used keys are saved to dedrm.json in the
working directory (gitignored — contains private cryptographic material).
Use --config to specify a stable path. On Linux, populate dedrm.json manually
by transferring keys from a Windows/Mac machine.

## Files changed vs upstream

Modified:
- DeDRM_plugin/__calibre_compat_code.py  (standalone import hook, else-branch only)
- DeDRM_plugin/prefs.py                  (PLUGIN_NAME import fix)
- DeDRM_plugin/standalone/remove_drm.py  (full implementation, was a stub)
- .gitignore                             (zip artifacts, dedrm.json, .claude/)

Added:
- dedrm.py              Entry point: dep check + stale-zip rebuild + delegation
- build_plugin.py       Fast zip builder for development
- requirements.txt      Python dependencies for standalone use
- STANDALONE_CLI.md     Usage, architecture, and design decisions
- dedrm_revision_plan.md  Implementation plan (dev reference)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a dedicated "Using as an Agent Skill" section to STANDALONE_CLI.md
covering the full trust chain an AI agent follows when pulling this repo
as a skill:

  clone → read source → audit → call dedrm.py → auto-build → run

Key design decisions documented:
- DeDRM_plugin.zip is never committed — it is a local build cache derived
  from the audited source, rebuilt on demand and on source staleness
- dedrm.py is the single entry point (~80 lines): dep install, stale-zip
  detection, and delegation via subprocess — no magic, fully readable
- Includes a JSON skill descriptor suitable for agent skill registries

Also updates dedrm_revision_plan.md to mark the plan as implemented and
records the extended agent-skill goals that emerged during design review,
explaining why the zip is gitignored rather than committed.

Verified: python3 dedrm.py correctly decrypts an Adobe ADEPT ePub
(Unwanted.epub) in 0.8 s via UUID-matched ADE key auto-discovery.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dedrm.py already auto-installs dependencies on first run.
Telling users to run pip install manually contradicted the
single-command design and was confusing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fix: standalone/__init__.py execute_action was silently dropping
the return value of perform_action(), so the process always exited 0
even on decryption failure. Fixed with sys.exit(perform_action(...)).
Verified: missing-file → exit 1, successful decrypt → exit 0.

Doc corrections in STANDALONE_CLI.md:
- Key lookup order was reversed: stored keys are tried FIRST,
  auto-discovery is the fallback (not the other way around)
- Progress messages go to stdout, not stderr (only arg errors → stderr)
- Module count corrected: ~48 .py files (was "~15")
- Added Windows glob caveat (*.epub does not expand in cmd/PowerShell)
- Removed duplicate transparency content between Agent Skill and
  Architecture sections
- Config-missing behaviour documented (warns and continues with defaults)
- Line count estimates updated to match actual file sizes
- Files Changed table updated to include the __init__.py exit-code fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant