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": {
"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"

View file

@ -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";

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

View file

@ -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__":