better build process, better output, libgourou updated, better file dir structure
This commit is contained in:
parent
cf007616b7
commit
094fac4d9f
6 changed files with 169 additions and 114 deletions
18
flake.lock
18
flake.lock
|
@ -7,11 +7,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1627256311,
|
"lastModified": 1628616743,
|
||||||
"narHash": "sha256-NEDsexz3lNmaLm6nSHURsmgNX592b3fcNR2fzEuk3U0=",
|
"narHash": "sha256-XNka+o/55SYOtDUXXOTmDvX4Qiqbaz4COq+JZ8MPoe8=",
|
||||||
"owner": "BentonEdmondson",
|
"owner": "BentonEdmondson",
|
||||||
"repo": "inept-epub",
|
"repo": "inept-epub",
|
||||||
"rev": "b992adc8be351c00673a032df10552d7bc577290",
|
"rev": "d6ba98a7159f61e7b77dc17c8c6bb62e193c8831",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -27,11 +27,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1627508403,
|
"lastModified": 1631412686,
|
||||||
"narHash": "sha256-JHOxPSJSaS+0NTUkNv6a9oI8g30pBuCDC+JwtZBaq3U=",
|
"narHash": "sha256-u0v3qU+qckvZE/obCHjeyktcvH8Wyo4Zld9AxEI53bI=",
|
||||||
"owner": "BentonEdmondson",
|
"owner": "BentonEdmondson",
|
||||||
"repo": "libgourou-utils",
|
"repo": "libgourou-utils",
|
||||||
"rev": "f16d3df200134209a1e2e530db056b3cd715425d",
|
"rev": "88d6dc2b549a28f886a15a7992890d60e576716d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -42,11 +42,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1627128856,
|
"lastModified": 1631206977,
|
||||||
"narHash": "sha256-yw3lA8zyNFhj309lmxvNByEEymRT1rRy5oE+jEPnsP4=",
|
"narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "dd14e5d78e90a2ccd6007e569820de9b4861a6c2",
|
"rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
27
flake.nix
27
flake.nix
|
@ -14,28 +14,25 @@
|
||||||
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;
|
||||||
in {
|
in {
|
||||||
defaultPackage.x86_64-linux = nixpkgs.stdenv.mkDerivation {
|
defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication {
|
||||||
pname = "knock";
|
pname = "knock";
|
||||||
version = "1.0.0-alpha";
|
version = "1.0.0-alpha";
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
nativeBuildInputs = [ nixpkgs.makeWrapper ];
|
propagatedBuildInputs = [
|
||||||
|
nixpkgs.python3Packages.python_magic
|
||||||
buildInputs = [
|
nixpkgs.python3Packages.xdg
|
||||||
(nixpkgs.python3.withPackages
|
nixpkgs.python3Packages.click
|
||||||
(python3Packages: [
|
libgourou-utils
|
||||||
python3Packages.python_magic
|
inept-epub
|
||||||
python3Packages.xdg
|
|
||||||
])
|
|
||||||
)
|
|
||||||
libgourou-utils inept-epub
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
format = "other";
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin $out/${nixpkgs.python3.sitePackages}
|
||||||
chmod +x knock
|
cp lib/*.py $out/${nixpkgs.python3.sitePackages}
|
||||||
cp knock $out/bin
|
cp src/knock.py $out/bin/knock
|
||||||
wrapProgram $out/bin/knock --prefix PATH : ${nixpkgs.lib.makeBinPath [libgourou-utils inept-epub]}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
|
|
90
knock
90
knock
|
@ -1,90 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os, sys, argparse, subprocess, magic, shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from getpass import getpass
|
|
||||||
from xdg import xdg_config_home
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog='knock', description='Convert an ACSM file to a DRM-free EPUB file')
|
|
||||||
parser.add_argument('acsm_file', metavar='file', type=str, help='the ACSM file to convert')
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# these are all Path objects: https://docs.python.org/3/library/pathlib.html
|
|
||||||
args.acsm_file = Path(args.acsm_file).expanduser().resolve()
|
|
||||||
args.drm_file = args.acsm_file.with_suffix('.drm')
|
|
||||||
args.epub_file = args.acsm_file.with_suffix('.epub')
|
|
||||||
args.adobe_dir = xdg_config_home() / 'knock'
|
|
||||||
|
|
||||||
if not args.acsm_file.exists():
|
|
||||||
sys.exit(f'ERROR: {str(args.acsm_file)} does not exist.')
|
|
||||||
|
|
||||||
if args.epub_file.exists():
|
|
||||||
sys.exit(f'ERROR: {str(args.epub_file)} already exists.')
|
|
||||||
|
|
||||||
if args.drm_file.exists():
|
|
||||||
sys.exit(f'ERROR: {str(args.drm_file)} must be moved or deleted.')
|
|
||||||
|
|
||||||
if not args.adobe_dir.exists():
|
|
||||||
print('This device is not registered with Adobe.')
|
|
||||||
email = input("Enter your Adobe account's email address: ")
|
|
||||||
password = getpass("Enter your Adobe account's password: ")
|
|
||||||
print('Registering this device with Adobe...')
|
|
||||||
|
|
||||||
result = subprocess.run([
|
|
||||||
'adept-register',
|
|
||||||
'-u', email,
|
|
||||||
'-O', str(args.adobe_dir)
|
|
||||||
], input=password.encode(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
if result.stdout.decode().strip().startswith('Exception code : 0x1003'):
|
|
||||||
shutil.rmtree(str(args.adobe_dir))
|
|
||||||
sys.exit('ERROR: Incorrect password')
|
|
||||||
|
|
||||||
if result.stdout.decode().strip().startswith('Exception code : 0x500a'):
|
|
||||||
shutil.rmtree(str(args.adobe_dir))
|
|
||||||
sys.exit('ERROR: No internet access')
|
|
||||||
|
|
||||||
if result.returncode != 0 or not args.adobe_dir.exists() or result.stdout.decode().strip().startswith('Exception code : '):
|
|
||||||
if args.adobe_dir.exists():
|
|
||||||
shutil.rmtree(str(args.adobe_dir))
|
|
||||||
print('ERROR: ', file=sys.stderr)
|
|
||||||
sys.exit(result)
|
|
||||||
|
|
||||||
print('Downloading the EPUB file from Adobe...')
|
|
||||||
|
|
||||||
result = subprocess.run([
|
|
||||||
'adept-download',
|
|
||||||
'-d', str(args.adobe_dir.joinpath('device.xml')),
|
|
||||||
'-a', str(args.adobe_dir.joinpath('activation.xml')),
|
|
||||||
'-k', str(args.adobe_dir.joinpath('devicesalt')),
|
|
||||||
'-o', str(args.drm_file),
|
|
||||||
'-f', str(args.acsm_file)
|
|
||||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
if result.stdout.decode().strip().startswith('Exception code : 0x500a'):
|
|
||||||
sys.exit('ERROR: No internet access')
|
|
||||||
|
|
||||||
if result.returncode != 0 or not args.drm_file.exists():
|
|
||||||
print('ERROR: ', file=sys.stderr)
|
|
||||||
sys.exit(result)
|
|
||||||
|
|
||||||
drm_file_type = magic.from_file(str(args.drm_file), mime=True)
|
|
||||||
if drm_file_type != 'application/epub+zip':
|
|
||||||
sys.exit(f'Received a file of type:\n{drm_file_type}\nKnock only supports EPUB files.')
|
|
||||||
|
|
||||||
print('Decrypting the file...')
|
|
||||||
|
|
||||||
result = subprocess.run([
|
|
||||||
'inept-epub',
|
|
||||||
str(args.adobe_dir.joinpath('activation.xml')),
|
|
||||||
str(args.drm_file),
|
|
||||||
str(args.epub_file)
|
|
||||||
], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
if result.returncode != 0 or not args.epub_file.exists():
|
|
||||||
print('ERROR: ', file=sys.stderr)
|
|
||||||
sys.exit(result)
|
|
||||||
|
|
||||||
args.drm_file.unlink()
|
|
||||||
|
|
||||||
print(f'\nDRM-free EPUB file created:\n{str(args.epub_file)}')
|
|
72
lib/handle_acsm.py
Normal file
72
lib/handle_acsm.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
from xdg import xdg_config_home
|
||||||
|
import click, sys, shutil, subprocess
|
||||||
|
from run import run
|
||||||
|
|
||||||
|
def handle_acsm(acsm_path):
|
||||||
|
drm_path = acsm_path.with_suffix('.drm')
|
||||||
|
adobe_dir = xdg_config_home() / 'knock' / 'acsm'
|
||||||
|
|
||||||
|
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()
|
||||||
|
):
|
||||||
|
shutil.rmtree(str(adobe_dir))
|
||||||
|
click.echo('This device is not registered with Adobe.')
|
||||||
|
email = click.prompt("Enter your Adobe account's email address")
|
||||||
|
password = click.prompt("Enter your Adobe account's password", hide_input=True)
|
||||||
|
click.echo('Registering this device with Adobe...')
|
||||||
|
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
'adept-register',
|
||||||
|
'-u', email,
|
||||||
|
'-O', str(adobe_dir)
|
||||||
|
],
|
||||||
|
stdin=password+'\n',
|
||||||
|
cleanser=lambda:shutil.rmtree(str(adobe_dir))
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo('Downloading the EPUB file from Adobe...')
|
||||||
|
|
||||||
|
run([
|
||||||
|
'adept-download',
|
||||||
|
'-d', str(adobe_dir.joinpath('device.xml')),
|
||||||
|
'-a', str(adobe_dir.joinpath('activation.xml')),
|
||||||
|
'-k', str(adobe_dir.joinpath('devicesalt')),
|
||||||
|
'-o', str(drm_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'
|
||||||
|
else:
|
||||||
|
click.echo(f'Error: Received file of media type {drm_file_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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
click.echo('Decrypting the file...')
|
||||||
|
|
||||||
|
run([
|
||||||
|
decryption_command,
|
||||||
|
str(args.adobe_dir.joinpath('activation.xml')),
|
||||||
|
str(args.drm_file),
|
||||||
|
str(args.epub_file)
|
||||||
|
])
|
||||||
|
|
||||||
|
args.drm_file.unlink()
|
||||||
|
|
||||||
|
click.secho(f'DRM-free EPUB file created:\n{str(args.epub_file)}', color='green')
|
33
lib/run.py
Normal file
33
lib/run.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import click, subprocess, sys
|
||||||
|
|
||||||
|
# run a command and display output in a styled terminal
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
input=stdin.encode(),
|
||||||
|
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)
|
||||||
|
|
||||||
|
# newline
|
||||||
|
click.echo('')
|
||||||
|
|
||||||
|
if result.returncode > 0:
|
||||||
|
cleanser()
|
||||||
|
click.echo(f'Error: Command returned error code {result.returncode}.', err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return result.returncode
|
43
src/knock.py
Executable file
43
src/knock.py
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
__version__ = "1.0.0-alpha"
|
||||||
|
|
||||||
|
@click.version_option()
|
||||||
|
@click.command()
|
||||||
|
@click.argument(
|
||||||
|
"file",
|
||||||
|
type=click.Path(
|
||||||
|
exists=True,
|
||||||
|
file_okay=True,
|
||||||
|
dir_okay=False,
|
||||||
|
readable=True,
|
||||||
|
resolve_path=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def main(file):
|
||||||
|
file = Path(file)
|
||||||
|
|
||||||
|
# make the config dir if it doesn't exist
|
||||||
|
(xdg_config_home() / 'knock').mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
file_type = file.suffix[1:].upper()
|
||||||
|
|
||||||
|
if file_type == 'ACSM':
|
||||||
|
click.echo('Received an ACSM (Adobe) file...')
|
||||||
|
handle_acsm(file)
|
||||||
|
else:
|
||||||
|
click.echo(f'Error: Files of type {file.suffix[1:].upper()} 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('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)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in a new issue