AAX is supported!

This commit is contained in:
benton 2021-09-12 17:46:15 -05:00
parent 0fc3a838d4
commit 2ecca95d21
6 changed files with 160 additions and 47 deletions

View file

@ -1,5 +1,25 @@
{ {
"nodes": { "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": { "inept-epub": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
@ -42,11 +62,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1631206977, "lastModified": 1631381596,
"narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", "narHash": "sha256-Xk91RO0uMyul8fWo3RP7WqEP5bsKUVucJRgLZgascAo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", "rev": "f6e9e908ccbcabb365cb5434a4a38dd8c996fc72",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -58,6 +78,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"benpkgs": "benpkgs",
"inept-epub": "inept-epub", "inept-epub": "inept-epub",
"libgourou-utils": "libgourou-utils", "libgourou-utils": "libgourou-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"

View file

@ -7,12 +7,16 @@
inept-epub.url = "github:BentonEdmondson/inept-epub"; inept-epub.url = "github:BentonEdmondson/inept-epub";
inept-epub.inputs.nixpkgs.follows = "nixpkgs"; inept-epub.inputs.nixpkgs.follows = "nixpkgs";
benpkgs.url = "github:BentonEdmondson/benpkgs";
benpkgs.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, ... }@flakes: let outputs = { self, ... }@flakes: let
nixpkgs = flakes.nixpkgs.legacyPackages.x86_64-linux; nixpkgs = flakes.nixpkgs.legacyPackages.x86_64-linux;
libgourou-utils = flakes.libgourou-utils.defaultPackage.x86_64-linux; libgourou-utils = flakes.libgourou-utils.defaultPackage.x86_64-linux;
inept-epub = flakes.inept-epub.defaultPackage.x86_64-linux; inept-epub = flakes.inept-epub.defaultPackage.x86_64-linux;
benpkgs = flakes.benpkgs.packages.x86_64-linux;
in { in {
defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication { defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication {
pname = "knock"; pname = "knock";
@ -25,6 +29,8 @@
nixpkgs.python3Packages.click nixpkgs.python3Packages.click
libgourou-utils libgourou-utils
inept-epub inept-epub
benpkgs.Audible
benpkgs.AAXtoMP3
]; ];
format = "other"; format = "other";

69
lib/handle_aax.py Normal file
View file

@ -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')

View file

@ -1,21 +1,20 @@
from xdg import xdg_config_home from xdg import xdg_config_home
import click, sys, shutil, subprocess import click, sys, shutil, subprocess
from run import run from utils import run
def handle_acsm(acsm_path): def handle_acsm(acsm_path):
drm_path = acsm_path.with_suffix('.drm') 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(): if drm_path.exists():
click.echo(f"Error: {drm_path} must be moved out of the way or deleted.", err=True) click.echo(f"Error: {drm_path} must be moved out of the way or deleted.", err=True)
sys.exit(1) sys.exit(1)
adobe_dir.mkdir(parents=True, exist_ok=True)
if ( if (
not (adobe_dir / 'device.xml').exists() not adobe_dir.joinpath('device.xml').exists()
or not (adobe_dir / 'activation.xml').exists() or not adobe_dir.joinpath('activation.xml').exists()
or not (adobe_dir / 'devicesalt').exists() or not adobe_dir.joinpath('devicesalt').exists()
): ):
shutil.rmtree(str(adobe_dir)) shutil.rmtree(str(adobe_dir))
click.echo('This device is not registered with Adobe.') 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)) cleanser=lambda:shutil.rmtree(str(adobe_dir))
) )
click.echo('Downloading the EPUB file from Adobe...') click.echo('Downloading the book from Adobe...')
run([ run([
'adept-download', 'adept-download',
@ -44,29 +43,34 @@ def handle_acsm(acsm_path):
'-f', str(acsm_path) '-f', str(acsm_path)
]) ])
drm_file_type = magic.from_file(str(args.drm_file), mime=True) drm_path_type = magic.from_file(str(drm_path), mime=True)
if drm_file_type == 'application/epub+zip': if drm_path_type == 'application/epub+zip':
decryption_command = 'inept-epub' file_type = 'EPUB'
elif drm_file_type == 'application/pdf': elif drm_path_type == 'application/pdf':
decryption_command = 'inept-pdf' file_type = 'PDF'
else: 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('Only the following ACSM conversions are currently supported:', err=True)
click.echo(' * ACSM -> EPUB', err=True) click.echo(' * ACSM -> EPUB', err=True)
click.echo(' * ACSM -> PDF', err=True) click.echo(' * ACSM -> PDF', err=True)
click.echo('Please open a feature request at:', 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) sys.exit(1)
click.echo('Decrypting the file...') click.echo('Decrypting the file...')
run([ run([
decryption_command, 'inept-' + file_type.lower(),
str(args.adobe_dir / 'activation.xml'), str(adobe_dir.joinpath('activation.xml')),
str(args.drm_file), str(drm_path),
str(args.epub_file) 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') click.secho(f'DRM-free {file_type} file created:\n{output_file}', fg='green')

View file

@ -4,14 +4,8 @@ import click, subprocess, sys
# cleanser is called if the command returns a >0 exit code # cleanser is called if the command returns a >0 exit code
def run(command: [str], stdin: str = '', cleanser = lambda: None) -> int: def run(command: [str], stdin: str = '', cleanser = lambda: None) -> int:
# newline and set styles open_fake_terminal(' '.join(command))
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( result = subprocess.run(
command, command,
stderr=subprocess.STDOUT, 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 check=False # don't throw Python error if returncode isn't 0
) )
# show returncode in bold, then reset styles close_fake_terminal(result.returncode, cleanser)
click.secho(f'\nknock[{result.returncode}]>', bold=True)
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 # newline
click.echo('') click.echo('')
if result.returncode > 0: if exit_code > 0:
cleanser() 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) sys.exit(1)
return result.returncode

View file

@ -3,15 +3,17 @@
import subprocess, magic, shutil, click import subprocess, magic, shutil, click
from pathlib import Path from pathlib import Path
from getpass import getpass from getpass import getpass
from handle_acsm import handle_acsm
from xdg import xdg_config_home from xdg import xdg_config_home
from handle_acsm import handle_acsm
from handle_aax import handle_aax
__version__ = "1.0.0-alpha" __version__ = "1.0.0-alpha"
@click.version_option() @click.version_option()
@click.command() @click.command()
@click.argument( @click.argument(
"file", "path",
type=click.Path( type=click.Path(
exists=True, exists=True,
file_okay=True, file_okay=True,
@ -20,23 +22,27 @@ __version__ = "1.0.0-alpha"
resolve_path=True resolve_path=True
) )
) )
def main(file): def main(path):
file = Path(file) path = Path(path)
# make the config dir if it doesn't exist # 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...') 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: 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('Only the following file types are currently supported:', err=True)
click.echo(' * ACSM (Adobe)\n') click.echo(' * ACSM (Adobe)\n')
click.echo(' * AAX (Audible)\n')
click.echo('Please open a feature request at:', 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{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) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":