AAX is supported!
This commit is contained in:
parent
0fc3a838d4
commit
2ecca95d21
6 changed files with 160 additions and 47 deletions
27
flake.lock
27
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"
|
||||
|
|
|
@ -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
69
lib/handle_aax.py
Normal 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')
|
|
@ -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')
|
|
@ -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
|
26
src/knock.py
26
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__":
|
||||
|
|
Loading…
Reference in a new issue