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": {
|
"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"
|
||||||
|
|
|
@ -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
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
|
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')
|
|
@ -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
|
|
26
src/knock.py
26
src/knock.py
|
@ -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__":
|
||||||
|
|
Loading…
Reference in a new issue