From 094fac4d9fc2ab7912d882f29827bafdee06ae6a Mon Sep 17 00:00:00 2001 From: benton Date: Sat, 11 Sep 2021 22:25:19 -0500 Subject: [PATCH] better build process, better output, libgourou updated, better file dir structure --- flake.lock | 18 +++++----- flake.nix | 27 +++++++------- knock | 90 ---------------------------------------------- lib/handle_acsm.py | 72 +++++++++++++++++++++++++++++++++++++ lib/run.py | 33 +++++++++++++++++ src/knock.py | 43 ++++++++++++++++++++++ 6 files changed, 169 insertions(+), 114 deletions(-) delete mode 100644 knock create mode 100644 lib/handle_acsm.py create mode 100644 lib/run.py create mode 100755 src/knock.py diff --git a/flake.lock b/flake.lock index 91c9d7c..2a84fbc 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1627256311, - "narHash": "sha256-NEDsexz3lNmaLm6nSHURsmgNX592b3fcNR2fzEuk3U0=", + "lastModified": 1628616743, + "narHash": "sha256-XNka+o/55SYOtDUXXOTmDvX4Qiqbaz4COq+JZ8MPoe8=", "owner": "BentonEdmondson", "repo": "inept-epub", - "rev": "b992adc8be351c00673a032df10552d7bc577290", + "rev": "d6ba98a7159f61e7b77dc17c8c6bb62e193c8831", "type": "github" }, "original": { @@ -27,11 +27,11 @@ ] }, "locked": { - "lastModified": 1627508403, - "narHash": "sha256-JHOxPSJSaS+0NTUkNv6a9oI8g30pBuCDC+JwtZBaq3U=", + "lastModified": 1631412686, + "narHash": "sha256-u0v3qU+qckvZE/obCHjeyktcvH8Wyo4Zld9AxEI53bI=", "owner": "BentonEdmondson", "repo": "libgourou-utils", - "rev": "f16d3df200134209a1e2e530db056b3cd715425d", + "rev": "88d6dc2b549a28f886a15a7992890d60e576716d", "type": "github" }, "original": { @@ -42,11 +42,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1627128856, - "narHash": "sha256-yw3lA8zyNFhj309lmxvNByEEymRT1rRy5oE+jEPnsP4=", + "lastModified": 1631206977, + "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dd14e5d78e90a2ccd6007e569820de9b4861a6c2", + "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fe432c6..4ade5c6 100644 --- a/flake.nix +++ b/flake.nix @@ -14,28 +14,25 @@ libgourou-utils = flakes.libgourou-utils.defaultPackage.x86_64-linux; inept-epub = flakes.inept-epub.defaultPackage.x86_64-linux; in { - defaultPackage.x86_64-linux = nixpkgs.stdenv.mkDerivation { + defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication { pname = "knock"; version = "1.0.0-alpha"; src = self; - nativeBuildInputs = [ nixpkgs.makeWrapper ]; - - buildInputs = [ - (nixpkgs.python3.withPackages - (python3Packages: [ - python3Packages.python_magic - python3Packages.xdg - ]) - ) - libgourou-utils inept-epub + propagatedBuildInputs = [ + nixpkgs.python3Packages.python_magic + nixpkgs.python3Packages.xdg + nixpkgs.python3Packages.click + libgourou-utils + inept-epub ]; + format = "other"; + installPhase = '' - mkdir -p $out/bin - chmod +x knock - cp knock $out/bin - wrapProgram $out/bin/knock --prefix PATH : ${nixpkgs.lib.makeBinPath [libgourou-utils inept-epub]} + mkdir -p $out/bin $out/${nixpkgs.python3.sitePackages} + cp lib/*.py $out/${nixpkgs.python3.sitePackages} + cp src/knock.py $out/bin/knock ''; meta = { diff --git a/knock b/knock deleted file mode 100644 index f037704..0000000 --- a/knock +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env python3 - -import os, sys, argparse, subprocess, magic, shutil -from pathlib import Path -from getpass import getpass -from xdg import xdg_config_home - -parser = argparse.ArgumentParser(prog='knock', description='Convert an ACSM file to a DRM-free EPUB file') -parser.add_argument('acsm_file', metavar='file', type=str, help='the ACSM file to convert') -args = parser.parse_args() - -# these are all Path objects: https://docs.python.org/3/library/pathlib.html -args.acsm_file = Path(args.acsm_file).expanduser().resolve() -args.drm_file = args.acsm_file.with_suffix('.drm') -args.epub_file = args.acsm_file.with_suffix('.epub') -args.adobe_dir = xdg_config_home() / 'knock' - -if not args.acsm_file.exists(): - sys.exit(f'ERROR: {str(args.acsm_file)} does not exist.') - -if args.epub_file.exists(): - sys.exit(f'ERROR: {str(args.epub_file)} already exists.') - -if args.drm_file.exists(): - sys.exit(f'ERROR: {str(args.drm_file)} must be moved or deleted.') - -if not args.adobe_dir.exists(): - print('This device is not registered with Adobe.') - email = input("Enter your Adobe account's email address: ") - password = getpass("Enter your Adobe account's password: ") - print('Registering this device with Adobe...') - - result = subprocess.run([ - 'adept-register', - '-u', email, - '-O', str(args.adobe_dir) - ], input=password.encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) - - if result.stdout.decode().strip().startswith('Exception code : 0x1003'): - shutil.rmtree(str(args.adobe_dir)) - sys.exit('ERROR: Incorrect password') - - if result.stdout.decode().strip().startswith('Exception code : 0x500a'): - shutil.rmtree(str(args.adobe_dir)) - sys.exit('ERROR: No internet access') - - if result.returncode != 0 or not args.adobe_dir.exists() or result.stdout.decode().strip().startswith('Exception code : '): - if args.adobe_dir.exists(): - shutil.rmtree(str(args.adobe_dir)) - print('ERROR: ', file=sys.stderr) - sys.exit(result) - -print('Downloading the EPUB file from Adobe...') - -result = subprocess.run([ - 'adept-download', - '-d', str(args.adobe_dir.joinpath('device.xml')), - '-a', str(args.adobe_dir.joinpath('activation.xml')), - '-k', str(args.adobe_dir.joinpath('devicesalt')), - '-o', str(args.drm_file), - '-f', str(args.acsm_file) -], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - -if result.stdout.decode().strip().startswith('Exception code : 0x500a'): - sys.exit('ERROR: No internet access') - -if result.returncode != 0 or not args.drm_file.exists(): - print('ERROR: ', file=sys.stderr) - sys.exit(result) - -drm_file_type = magic.from_file(str(args.drm_file), mime=True) -if drm_file_type != 'application/epub+zip': - sys.exit(f'Received a file of type:\n{drm_file_type}\nKnock only supports EPUB files.') - -print('Decrypting the file...') - -result = subprocess.run([ - 'inept-epub', - str(args.adobe_dir.joinpath('activation.xml')), - str(args.drm_file), - str(args.epub_file) -], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - -if result.returncode != 0 or not args.epub_file.exists(): - print('ERROR: ', file=sys.stderr) - sys.exit(result) - -args.drm_file.unlink() - -print(f'\nDRM-free EPUB file created:\n{str(args.epub_file)}') \ No newline at end of file diff --git a/lib/handle_acsm.py b/lib/handle_acsm.py new file mode 100644 index 0000000..f1a7fae --- /dev/null +++ b/lib/handle_acsm.py @@ -0,0 +1,72 @@ +from xdg import xdg_config_home +import click, sys, shutil, subprocess +from run import run + +def handle_acsm(acsm_path): + drm_path = acsm_path.with_suffix('.drm') + adobe_dir = xdg_config_home() / 'knock' / 'acsm' + + if drm_path.exists(): + click.echo(f"Error: {drm_path} must be moved out of the way or deleted.", err=True) + sys.exit(1) + + adobe_dir.mkdir(parents=True, exist_ok=True) + + if ( + not (adobe_dir / 'device.xml').exists() + or not (adobe_dir / 'activation.xml').exists() + or not (adobe_dir / 'devicesalt').exists() + ): + shutil.rmtree(str(adobe_dir)) + click.echo('This device is not registered with Adobe.') + email = click.prompt("Enter your Adobe account's email address") + password = click.prompt("Enter your Adobe account's password", hide_input=True) + click.echo('Registering this device with Adobe...') + + run( + [ + 'adept-register', + '-u', email, + '-O', str(adobe_dir) + ], + stdin=password+'\n', + cleanser=lambda:shutil.rmtree(str(adobe_dir)) + ) + + click.echo('Downloading the EPUB file from Adobe...') + + run([ + 'adept-download', + '-d', str(adobe_dir.joinpath('device.xml')), + '-a', str(adobe_dir.joinpath('activation.xml')), + '-k', str(adobe_dir.joinpath('devicesalt')), + '-o', str(drm_path), + '-f', str(acsm_path) + ]) + + drm_file_type = magic.from_file(str(args.drm_file), mime=True) + if drm_file_type == 'application/epub+zip': + decryption_command = 'inept-epub' + elif drm_file_type == 'application/pdf': + decryption_command = 'inept-pdf' + else: + click.echo(f'Error: Received file of media type {drm_file_type}.', err=True) + click.echo('Only the following ACSM conversions are currently supported:', err=True) + click.echo(' * ACSM -> EPUB', err=True) + click.echo(' * ACSM -> PDF', err=True) + click.echo('Please open a feature request at:', err=True) + click.echo(f' https://github.com/BentonEdmondson/knock/issues/new?title=Support%20{drm_file_type}%20Files&labels=enhancement', err=True) + sys.exit(1) + + click.echo('Decrypting the file...') + + run([ + decryption_command, + str(args.adobe_dir.joinpath('activation.xml')), + str(args.drm_file), + str(args.epub_file) + ]) + + args.drm_file.unlink() + + click.secho(f'DRM-free EPUB file created:\n{str(args.epub_file)}', color='green') \ No newline at end of file diff --git a/lib/run.py b/lib/run.py new file mode 100644 index 0000000..4472719 --- /dev/null +++ b/lib/run.py @@ -0,0 +1,33 @@ +import click, subprocess, sys + +# run a command and display output in a styled terminal +# cleanser is called if the command returns a >0 exit code +def run(command: [str], stdin: str = '', cleanser = lambda: None) -> int: + + # newline and set styles + click.secho('', fg='white', bg='black', bold=True, reset=False) + + # show command + click.echo('knock> ' + ' '.join(command)) + + # remove bold + click.secho('', fg='white', bg='black', bold=False, reset=False) + result = subprocess.run( + command, + stderr=subprocess.STDOUT, + input=stdin.encode(), + check=False # don't throw Python error if returncode isn't 0 + ) + + # show returncode in bold, then reset styles + click.secho(f'\nknock[{result.returncode}]>', bold=True) + + # newline + click.echo('') + + if result.returncode > 0: + cleanser() + click.echo(f'Error: Command returned error code {result.returncode}.', err=True) + sys.exit(1) + + return result.returncode \ No newline at end of file diff --git a/src/knock.py b/src/knock.py new file mode 100755 index 0000000..bd63155 --- /dev/null +++ b/src/knock.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import subprocess, magic, shutil, click +from pathlib import Path +from getpass import getpass +from handle_acsm import handle_acsm +from xdg import xdg_config_home + +__version__ = "1.0.0-alpha" + +@click.version_option() +@click.command() +@click.argument( + "file", + type=click.Path( + exists=True, + file_okay=True, + dir_okay=False, + readable=True, + resolve_path=True + ) +) +def main(file): + file = Path(file) + + # make the config dir if it doesn't exist + (xdg_config_home() / 'knock').mkdir(parents=True, exist_ok=True) + + file_type = file.suffix[1:].upper() + + if file_type == 'ACSM': + click.echo('Received an ACSM (Adobe) file...') + handle_acsm(file) + else: + click.echo(f'Error: Files of type {file.suffix[1:].upper()} are not supported.\n', err=True) + click.echo('Only the following file types are currently supported:', err=True) + click.echo(' * ACSM (Adobe)\n') + click.echo('Please open a feature request at:', err=True) + click.echo(f' https://github.com/BentonEdmondson/knock/issues/new?title=Support%20{file_type}%20Files&labels=enhancement', err=True) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file