From 2ecca95d217d5e98aae2437afd1279eb2ad6d97f Mon Sep 17 00:00:00 2001 From: benton Date: Sun, 12 Sep 2021 17:46:15 -0500 Subject: [PATCH] AAX is supported! --- flake.lock | 27 ++++++++++++++-- flake.nix | 6 ++++ lib/handle_aax.py | 69 ++++++++++++++++++++++++++++++++++++++++ lib/handle_acsm.py | 46 +++++++++++++++------------ lib/{run.py => utils.py} | 33 +++++++++++-------- src/knock.py | 26 +++++++++------ 6 files changed, 160 insertions(+), 47 deletions(-) create mode 100644 lib/handle_aax.py rename lib/{run.py => utils.py} (60%) diff --git a/flake.lock b/flake.lock index 2a84fbc..757ebaf 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "benpkgs": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1631424389, + "narHash": "sha256-Nbs8FjqdJQwSBG8AjfOefqjbqVueO2o5HGz4/7YdQMM=", + "owner": "BentonEdmondson", + "repo": "benpkgs", + "rev": "d6cd8eab1c25cb1186de283f28c9afed97c49dd2", + "type": "github" + }, + "original": { + "owner": "BentonEdmondson", + "repo": "benpkgs", + "type": "github" + } + }, "inept-epub": { "inputs": { "nixpkgs": [ @@ -42,11 +62,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1631206977, - "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", + "lastModified": 1631381596, + "narHash": "sha256-Xk91RO0uMyul8fWo3RP7WqEP5bsKUVucJRgLZgascAo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", + "rev": "f6e9e908ccbcabb365cb5434a4a38dd8c996fc72", "type": "github" }, "original": { @@ -58,6 +78,7 @@ }, "root": { "inputs": { + "benpkgs": "benpkgs", "inept-epub": "inept-epub", "libgourou-utils": "libgourou-utils", "nixpkgs": "nixpkgs" diff --git a/flake.nix b/flake.nix index 4ade5c6..1454830 100644 --- a/flake.nix +++ b/flake.nix @@ -7,12 +7,16 @@ inept-epub.url = "github:BentonEdmondson/inept-epub"; inept-epub.inputs.nixpkgs.follows = "nixpkgs"; + + benpkgs.url = "github:BentonEdmondson/benpkgs"; + benpkgs.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, ... }@flakes: let nixpkgs = flakes.nixpkgs.legacyPackages.x86_64-linux; libgourou-utils = flakes.libgourou-utils.defaultPackage.x86_64-linux; inept-epub = flakes.inept-epub.defaultPackage.x86_64-linux; + benpkgs = flakes.benpkgs.packages.x86_64-linux; in { defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication { pname = "knock"; @@ -25,6 +29,8 @@ nixpkgs.python3Packages.click libgourou-utils inept-epub + benpkgs.Audible + benpkgs.AAXtoMP3 ]; format = "other"; diff --git a/lib/handle_aax.py b/lib/handle_aax.py new file mode 100644 index 0000000..698b071 --- /dev/null +++ b/lib/handle_aax.py @@ -0,0 +1,69 @@ +from xdg import xdg_config_home +from utils import open_fake_terminal, close_fake_terminal, run +import audible, click, sys + +def handle_aax(aax_path): + authcode_path = xdg_config_home().joinpath('knock', 'aax', 'authcode') + + # make the config dir if it doesn't exist + authcode_path.parent.mkdir(parents=True, exist_ok=True) + + m4b_path = aax_path.with_suffix('.m4b') + cover_path = aax_path.parent.joinpath('cover.jpg') + chapters_path = aax_path.with_suffix('.chapters.txt') + + if m4b_path.exists(): + click.echo(f"Error: {m4b_path} must be moved out of the way or deleted.", err=True) + sys.exit(1) + + if cover_path.exists(): + click.echo(f"Error: {cover_path} must be moved out of the way or deleted.", err=True) + sys.exit(1) + + if chapters_path.exists(): + click.echo(f"Error: {chapters_path} must be moved out of the way or deleted.", err=True) + sys.exit(1) + + if not authcode_path.exists(): + click.echo('This device does not have an Audible decryption key.') + + email = click.prompt("Enter your Audible account's email address") + password = click.prompt("Enter your Audible account's password", hide_input=True) + locale = click.prompt("Enter your locale (e.g. 'us', 'ca', 'jp', etc)") + + open_fake_terminal(f'Authenticator.from_login("{email}").get_activation_bytes()') + + try: + authcode = audible.auth.Authenticator.from_login( + username=email, + password=password, + locale=locale + ).get_activation_bytes() + authcode_path.write_text(authcode) + except Exception as error: + click.echo(error, err=True) + close_fake_terminal(1) + + close_fake_terminal(0) + + click.echo('Decrypting the file...') + + authcode = authcode_path.read_text() + + def cleanup(): + cover_path.unlink(missing_ok=True) + chapters_path.unlink(missing_ok=True) + + run([ + 'AAXtoMP3', + '--authcode', authcode, + '-e:m4b', + '-t', str(aax_path.parent), + '-D', '.', + '-F', str(aax_path.stem), + str(aax_path) + ], cleanser=cleanup) + + cleanup() + + click.secho(f'DRM-free M4B file created:\n{aax_path.with_suffix(".m4b")}', fg='green') diff --git a/lib/handle_acsm.py b/lib/handle_acsm.py index f0af3c0..8beeaea 100644 --- a/lib/handle_acsm.py +++ b/lib/handle_acsm.py @@ -1,21 +1,20 @@ from xdg import xdg_config_home import click, sys, shutil, subprocess -from run import run +from utils import run def handle_acsm(acsm_path): drm_path = acsm_path.with_suffix('.drm') - adobe_dir = xdg_config_home() / 'knock' / 'acsm' + adobe_dir = xdg_config_home().joinpath('knock', 'acsm') + adobe_dir.mkdir(parents=True, exist_ok=True) 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() + not adobe_dir.joinpath('device.xml').exists() + or not adobe_dir.joinpath('activation.xml').exists() + or not adobe_dir.joinpath('devicesalt').exists() ): shutil.rmtree(str(adobe_dir)) click.echo('This device is not registered with Adobe.') @@ -33,7 +32,7 @@ def handle_acsm(acsm_path): cleanser=lambda:shutil.rmtree(str(adobe_dir)) ) - click.echo('Downloading the EPUB file from Adobe...') + click.echo('Downloading the book from Adobe...') run([ 'adept-download', @@ -44,29 +43,34 @@ def handle_acsm(acsm_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' + drm_path_type = magic.from_file(str(drm_path), mime=True) + if drm_path_type == 'application/epub+zip': + file_type = 'EPUB' + elif drm_path_type == 'application/pdf': + file_type = 'PDF' else: - click.echo(f'Error: Received file of media type {drm_file_type}.', err=True) + click.echo(f'Error: Received file of media type {drm_path_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) + click.echo(f' https://github.com/BentonEdmondson/knock/issues/new?title=Support%20{drm_path_type}%20Files&labels=enhancement', err=True) + sys.exit(1) + + output_file = acsm_path.with_suffix(file_type) + if output_file.exists(): + click.echo(f"Error: {output_file} must be moved out of the way or deleted.", err=True) sys.exit(1) click.echo('Decrypting the file...') run([ - decryption_command, - str(args.adobe_dir / 'activation.xml'), - str(args.drm_file), - str(args.epub_file) + 'inept-' + file_type.lower(), + str(adobe_dir.joinpath('activation.xml')), + str(drm_path), + str(output_file) ]) - args.drm_file.unlink() + drm_path.unlink() - click.secho(f'DRM-free EPUB file created:\n{str(args.epub_file)}', color='green') \ No newline at end of file + click.secho(f'DRM-free {file_type} file created:\n{output_file}', fg='green') \ No newline at end of file diff --git a/lib/run.py b/lib/utils.py similarity index 60% rename from lib/run.py rename to lib/utils.py index 4472719..82cae6a 100644 --- a/lib/run.py +++ b/lib/utils.py @@ -4,14 +4,8 @@ import click, subprocess, sys # 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) + open_fake_terminal(' '.join(command)) - # 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, @@ -19,15 +13,28 @@ def run(command: [str], stdin: str = '', cleanser = lambda: None) -> int: 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) + close_fake_terminal(result.returncode, cleanser) + + return result.returncode + + +def open_fake_terminal(command: str): + click.secho('', fg='white', bg='black', bold=True, reset=False) + + # show command + click.echo(f'knock> {command}') + + # remove bold + click.secho('', fg='white', bg='black', bold=False, reset=False) + + +def close_fake_terminal(exit_code: int, cleanser = lambda: None): + click.secho(f'\nknock[{exit_code}]>', bold=True) # newline click.echo('') - if result.returncode > 0: + if exit_code > 0: cleanser() - click.echo(f'Error: Command returned error code {result.returncode}.', err=True) + click.echo(f'Error: Command returned error code {exit_code}.', err=True) sys.exit(1) - - return result.returncode \ No newline at end of file diff --git a/src/knock.py b/src/knock.py index bd63155..934b451 100755 --- a/src/knock.py +++ b/src/knock.py @@ -3,15 +3,17 @@ 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 +from handle_acsm import handle_acsm +from handle_aax import handle_aax + __version__ = "1.0.0-alpha" @click.version_option() @click.command() @click.argument( - "file", + "path", type=click.Path( exists=True, file_okay=True, @@ -20,23 +22,27 @@ __version__ = "1.0.0-alpha" resolve_path=True ) ) -def main(file): - file = Path(file) +def main(path): + path = Path(path) # make the config dir if it doesn't exist - (xdg_config_home() / 'knock').mkdir(parents=True, exist_ok=True) + xdg_config_home().joinpath('knock').mkdir(parents=True, exist_ok=True) - file_type = file.suffix[1:].upper() + path_type = path.suffix[1:].upper() - if file_type == 'ACSM': + if path_type == 'ACSM': click.echo('Received an ACSM (Adobe) file...') - handle_acsm(file) + handle_acsm(path) + elif path_type == 'AAX': + click.echo('Received an AAX (Audible) file...') + handle_aax(path) else: - click.echo(f'Error: Files of type {file.suffix[1:].upper()} are not supported.\n', err=True) + click.echo(f'Error: Files of type {path_type} 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(' * AAX (Audible)\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) + click.echo(f' https://github.com/BentonEdmondson/knock/issues/new?title=Support%20{path_type}%20Files&labels=enhancement', err=True) sys.exit(1) if __name__ == "__main__":