switched from python to cpp
This commit is contained in:
parent
52d863ecf5
commit
851ba90431
10 changed files with 335 additions and 350 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
result
|
result
|
||||||
|
.vscode
|
||||||
|
todo.md
|
BIN
demo.png
BIN
demo.png
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
95
flake.lock
95
flake.lock
|
@ -1,32 +1,45 @@
|
||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"benpkgs": {
|
"base64-src": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1638237664,
|
"lastModified": 1468170709,
|
||||||
"narHash": "sha256-9SkZPl1EZ1ezWZoMdhYOzPm2HK0Ca5hkfphjlPNk/FY=",
|
"narHash": "sha256-dt6i1j0rqH7lA+2XXp9KTEhj2GvYueyGrHh9VXBEsbw=",
|
||||||
"owner": "BentonEdmondson",
|
"ref": "master",
|
||||||
"repo": "benpkgs",
|
"rev": "7d5a89229a525452e37504976a73c35fbaf2fe4d",
|
||||||
"rev": "da71a3a488de3ae7ab8daff2c2a9354d2f0464bf",
|
"revCount": 1,
|
||||||
"type": "github"
|
"type": "git",
|
||||||
|
"url": "https://gist.github.com/f0fd86b6c73063283afe550bc5d77594.git"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "BentonEdmondson",
|
"type": "git",
|
||||||
"repo": "benpkgs",
|
"url": "https://gist.github.com/f0fd86b6c73063283afe550bc5d77594.git"
|
||||||
"type": "github"
|
}
|
||||||
|
},
|
||||||
|
"gourou-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1650729079,
|
||||||
|
"narHash": "sha256-2ZnuO/fIjSzTK0srFzDAhGNh1hA4lZf3lK4VuFxWXmo=",
|
||||||
|
"ref": "master",
|
||||||
|
"rev": "7b6b1471fefb27e79e06e5d686cb8842c539cd0c",
|
||||||
|
"revCount": 79,
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://soutade.fr/libgourou.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "master",
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://soutade.fr/libgourou.git"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1638198142,
|
"lastModified": 1654230545,
|
||||||
"narHash": "sha256-plU9b8r4St6q4U7VHtG9V7oF8k9fIpfXl/KDaZLuY9k=",
|
"narHash": "sha256-8Vlwf0x8ow6pPOK2a04bT+pxIeRnM1+O0Xv9/CuDzRs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8a308775674e178495767df90c419425474582a1",
|
"rev": "236cc2971ac72acd90f0ae3a797f9f83098b17ec",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -36,31 +49,47 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rmdrm": {
|
"pugixml-src": {
|
||||||
"inputs": {
|
"flake": false,
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1638240151,
|
"lastModified": 1644379750,
|
||||||
"narHash": "sha256-2FHEb4xaskyLdS5M/eILue74Idplve9g8FRxg9D1Z/4=",
|
"narHash": "sha256-FLemG9T17n6l7vgb01OmO22BK59jv5uozVHeUnILEEQ=",
|
||||||
"owner": "BentonEdmondson",
|
"owner": "zeux",
|
||||||
"repo": "rmdrm",
|
"repo": "pugixml",
|
||||||
"rev": "c60eaa0c4338be5ee6165f59f2a1816566aba8a0",
|
"rev": "314baf6605143f1e837209008f490e8559529e1c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "BentonEdmondson",
|
"owner": "zeux",
|
||||||
"repo": "rmdrm",
|
"ref": "latest",
|
||||||
|
"repo": "pugixml",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"benpkgs": "benpkgs",
|
"base64-src": "base64-src",
|
||||||
|
"gourou-src": "gourou-src",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"rmdrm": "rmdrm"
|
"pugixml-src": "pugixml-src",
|
||||||
|
"updfparser-src": "updfparser-src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"updfparser-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1647424063,
|
||||||
|
"narHash": "sha256-9dvibKiUbbI4CrmuAaJzlpntT0XdLvdGeC2/WzjlA5U=",
|
||||||
|
"ref": "master",
|
||||||
|
"rev": "9d56c1d0b1ce81aae4c8db9d99a8b5d1f7967bcf",
|
||||||
|
"revCount": 25,
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://soutade.fr/updfparser.git"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"ref": "master",
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://soutade.fr/updfparser.git"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
190
flake.nix
190
flake.nix
|
@ -1,60 +1,150 @@
|
||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
gourou-src = {
|
||||||
rmdrm.url = "github:BentonEdmondson/rmdrm";
|
url = "git://soutade.fr/libgourou.git";
|
||||||
rmdrm.inputs.nixpkgs.follows = "nixpkgs";
|
type = "git";
|
||||||
|
ref = "master";
|
||||||
benpkgs.url = "github:BentonEdmondson/benpkgs";
|
flake = false;
|
||||||
benpkgs.inputs.nixpkgs.follows = "nixpkgs";
|
};
|
||||||
|
updfparser-src = {
|
||||||
|
url = "git://soutade.fr/updfparser.git";
|
||||||
|
type = "git";
|
||||||
|
ref = "master";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
base64-src = {
|
||||||
|
url = "git+https://gist.github.com/f0fd86b6c73063283afe550bc5d77594.git";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
pugixml-src = {
|
||||||
|
url = "github:zeux/pugixml/latest";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = flakes: let
|
outputs = flakes: let
|
||||||
nixpkgs = flakes.nixpkgs.legacyPackages.x86_64-linux;
|
self = flakes.self.packages.x86_64-linux;
|
||||||
libgourou-utils = flakes.libgourou-utils.defaultPackage.x86_64-linux;
|
nixpkgs = flakes.nixpkgs.legacyPackages.x86_64-linux.pkgsStatic;
|
||||||
rmdrm = flakes.rmdrm.defaultPackage.x86_64-linux;
|
gourou-src = flakes.gourou-src;
|
||||||
benpkgs = flakes.benpkgs.packages.x86_64-linux;
|
updfparser-src = flakes.updfparser-src;
|
||||||
in {
|
base64-src = flakes.base64-src;
|
||||||
defaultPackage.x86_64-linux = nixpkgs.python3Packages.buildPythonApplication rec {
|
pugixml-src = flakes.pugixml-src;
|
||||||
pname = "knock";
|
cxx = "${nixpkgs.stdenv.cc}/bin/x86_64-unknown-linux-musl-g++";
|
||||||
version = "1.1.0-alpha";
|
ar = "${nixpkgs.stdenv.cc.bintools.bintools_bin}/bin/x86_64-unknown-linux-musl-ar";
|
||||||
src = ./.;
|
obj-flags = "-O2 -static";
|
||||||
|
in rec {
|
||||||
nativeBuildInputs = [ nixpkgs.makeWrapper ];
|
packages.x86_64-linux.libzip-static = nixpkgs.libzip.overrideAttrs (prev: {
|
||||||
|
cmakeFlags = (prev.cmakeFlags or []) ++ [
|
||||||
buildInputs = [
|
"-DBUILD_SHARED_LIBS=OFF"
|
||||||
rmdrm
|
"-DBUILD_EXAMPLES=OFF"
|
||||||
benpkgs.libgourou
|
"-DBUILD_DOC=OFF"
|
||||||
nixpkgs.ffmpeg
|
"-DBUILD_TOOLS=OFF"
|
||||||
|
"-DBUILD_REGRESS=OFF"
|
||||||
];
|
];
|
||||||
|
outputs = ["out"];
|
||||||
propagatedBuildInputs = [
|
});
|
||||||
benpkgs.Audible
|
packages.x86_64-linux.base64 = derivation {
|
||||||
nixpkgs.python3Packages.python_magic
|
name = "updfparser";
|
||||||
nixpkgs.python3Packages.xdg
|
system = "x86_64-linux";
|
||||||
nixpkgs.python3Packages.click
|
builder = "${nixpkgs.bash}/bin/bash";
|
||||||
];
|
PATH = "${nixpkgs.coreutils}/bin";
|
||||||
|
args = ["-c" ''
|
||||||
format = "other";
|
mkdir -p $out/include/base64
|
||||||
|
cp ${base64-src}/Base64.h $out/include/base64/Base64.h
|
||||||
installPhase = ''
|
''];
|
||||||
mkdir -p $out/bin $out/${nixpkgs.python3.sitePackages}
|
|
||||||
cp lib/*.py $out/${nixpkgs.python3.sitePackages}
|
|
||||||
cp src/knock.py $out/bin/knock
|
|
||||||
wrapProgram $out/bin/knock --prefix PATH : ${nixpkgs.lib.makeBinPath buildInputs}
|
|
||||||
#'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "A CLI tool to convert ACSM files to DRM-free EPUB/PDF files";
|
|
||||||
homepage = "https://github.com/BentonEdmondson/knock";
|
|
||||||
license = [ nixpkgs.lib.licenses.gpl3Only ];
|
|
||||||
maintainers = [{
|
|
||||||
name = "Benton Edmondson";
|
|
||||||
email = "bentonedmondson@gmail.com";
|
|
||||||
}];
|
|
||||||
# potentially others, but I'm only listed those tested
|
|
||||||
platforms = [ "x86_64-linux" ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
packages.x86_64-linux.updfparser = derivation {
|
||||||
|
name = "updfparser";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
builder = "${nixpkgs.bash}/bin/bash";
|
||||||
|
PATH = "${nixpkgs.coreutils}/bin";
|
||||||
|
args = [ "-c" ''
|
||||||
|
${cxx} \
|
||||||
|
-c ${updfparser-src}/src/*.cpp \
|
||||||
|
-I ${updfparser-src}/include \
|
||||||
|
${obj-flags}
|
||||||
|
mkdir -p $out/lib
|
||||||
|
${ar} crs $out/lib/libupdfparser.a *.o
|
||||||
|
'' ];
|
||||||
|
};
|
||||||
|
packages.x86_64-linux.gourou = derivation {
|
||||||
|
name = "gourou";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
builder = "${nixpkgs.bash}/bin/bash";
|
||||||
|
PATH = "${nixpkgs.coreutils}/bin";
|
||||||
|
args = [ "-c" ''
|
||||||
|
${cxx} \
|
||||||
|
-c \
|
||||||
|
${gourou-src}/src/*.cpp \
|
||||||
|
${pugixml-src}/src/pugixml.cpp \
|
||||||
|
-I ${self.base64}/include \
|
||||||
|
-I ${gourou-src}/include \
|
||||||
|
-I ${pugixml-src}/src \
|
||||||
|
-I ${updfparser-src}/include \
|
||||||
|
${obj-flags}
|
||||||
|
mkdir -p $out/lib $out/debug
|
||||||
|
${ar} crs $out/lib/libgourou.a *.o
|
||||||
|
cp *.o $out/debug
|
||||||
|
'' ];
|
||||||
|
};
|
||||||
|
packages.x86_64-linux.utils-common = derivation {
|
||||||
|
name = "utils-common";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
builder = "${nixpkgs.bash}/bin/bash";
|
||||||
|
PATH = "${nixpkgs.coreutils}/bin";
|
||||||
|
args = [ "-c" ''
|
||||||
|
${cxx} \
|
||||||
|
-c ${gourou-src}/utils/drmprocessorclientimpl.cpp \
|
||||||
|
${gourou-src}/utils/utils_common.cpp \
|
||||||
|
-I ${gourou-src}/utils \
|
||||||
|
-I ${gourou-src}/include \
|
||||||
|
-I ${pugixml-src}/src \
|
||||||
|
-I ${nixpkgs.openssl.dev}/include \
|
||||||
|
-I ${nixpkgs.curl.dev}/include \
|
||||||
|
-I ${nixpkgs.zlib.dev}/include \
|
||||||
|
-I ${self.libzip-static}/include \
|
||||||
|
${obj-flags}
|
||||||
|
mkdir -p $out/lib
|
||||||
|
${ar} crs $out/lib/libutils-common.a *.o
|
||||||
|
'' ];
|
||||||
|
};
|
||||||
|
packages.x86_64-linux.knock = derivation {
|
||||||
|
name = "knock";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
builder = "${nixpkgs.bash}/bin/bash";
|
||||||
|
PATH = "${nixpkgs.coreutils}/bin";
|
||||||
|
args = [ "-c" ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
${cxx} \
|
||||||
|
-o $out/bin/knock \
|
||||||
|
${./src/knock.cpp} \
|
||||||
|
-Wl,--as-needed -static \
|
||||||
|
${self.utils-common}/lib/libutils-common.a \
|
||||||
|
${self.gourou}/lib/libgourou.a \
|
||||||
|
${self.updfparser}/lib/libupdfparser.a \
|
||||||
|
-Wl,--start-group \
|
||||||
|
${self.libzip-static}/lib/libzip.a \
|
||||||
|
${nixpkgs.libnghttp2}/lib/libnghttp2.a \
|
||||||
|
${nixpkgs.libidn2.out}/lib/libidn2.a \
|
||||||
|
${nixpkgs.libunistring}/lib/libunistring.a \
|
||||||
|
${nixpkgs.libssh2}/lib/libssh2.a \
|
||||||
|
${nixpkgs.zstd.out}/lib/libzstd.a \
|
||||||
|
${nixpkgs.zlib}/lib/libz.a \
|
||||||
|
${nixpkgs.openssl.out}/lib/libcrypto.a \
|
||||||
|
${nixpkgs.curl.out}/lib/libcurl.a \
|
||||||
|
${nixpkgs.openssl.out}/lib/libssl.a \
|
||||||
|
-static-libgcc -static-libstdc++ \
|
||||||
|
-Wl,--end-group \
|
||||||
|
-I ${gourou-src}/utils \
|
||||||
|
-I ${gourou-src}/include \
|
||||||
|
-I ${pugixml-src}/src \
|
||||||
|
-I ${nixpkgs.openssl.dev}/include \
|
||||||
|
-I ${nixpkgs.curl.dev}/include \
|
||||||
|
-I ${nixpkgs.zlib.dev}/include \
|
||||||
|
-I ${self.libzip-static}/include
|
||||||
|
'' ];
|
||||||
|
};
|
||||||
|
defaultPackage.x86_64-linux = self.knock;
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
||||||
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 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'audible.auth.Authenticator.from_login("{email}", "{locale}").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()
|
|
||||||
|
|
||||||
run([
|
|
||||||
'ffmpeg',
|
|
||||||
'-activation_bytes', authcode,
|
|
||||||
'-i', str(aax_path),
|
|
||||||
'-c', 'copy', str(m4b_path),
|
|
||||||
'-loglevel', 'error'
|
|
||||||
])
|
|
||||||
|
|
||||||
click.secho(f'DRM-free M4B file created:\n{m4b_path}', fg='green')
|
|
|
@ -1,77 +0,0 @@
|
||||||
from xdg import xdg_config_home
|
|
||||||
import click, sys, shutil, subprocess, magic
|
|
||||||
from utils import run
|
|
||||||
|
|
||||||
def handle_acsm(acsm_path):
|
|
||||||
drm_path = acsm_path.with_suffix('.drm')
|
|
||||||
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)
|
|
||||||
|
|
||||||
if (
|
|
||||||
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.')
|
|
||||||
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_activate',
|
|
||||||
'-u', email,
|
|
||||||
'-O', str(adobe_dir)
|
|
||||||
],
|
|
||||||
stdin=password+'\n',
|
|
||||||
cleanser=lambda:shutil.rmtree(str(adobe_dir))
|
|
||||||
)
|
|
||||||
|
|
||||||
click.echo('Downloading the book from Adobe...')
|
|
||||||
|
|
||||||
run([
|
|
||||||
'acsmdownloader',
|
|
||||||
'-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_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_path_type} from Adobe\' servers.', 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_path_type}%20Files&labels=enhancement', err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
output_path = acsm_path.with_suffix('.' + file_type)
|
|
||||||
if output_path.exists():
|
|
||||||
drm_path.unlink()
|
|
||||||
click.echo(f"Error: {output_path} must be moved out of the way or deleted.", err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
click.echo('Decrypting the file...')
|
|
||||||
|
|
||||||
run([
|
|
||||||
'rmdrm-' + file_type,
|
|
||||||
str(adobe_dir.joinpath('activation.xml')),
|
|
||||||
str(drm_path),
|
|
||||||
str(output_path)
|
|
||||||
])
|
|
||||||
|
|
||||||
drm_path.unlink()
|
|
||||||
|
|
||||||
click.secho(f'DRM-free {file_type.upper()} file created:\n{output_path}', fg='green')
|
|
45
lib/utils.py
45
lib/utils.py
|
@ -1,45 +0,0 @@
|
||||||
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:
|
|
||||||
|
|
||||||
open_fake_terminal(' '.join(command))
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
command,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
input=stdin.encode(),
|
|
||||||
check=False # don't throw Python error if returncode isn't 0
|
|
||||||
)
|
|
||||||
|
|
||||||
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 exit_code > 0:
|
|
||||||
cleanser()
|
|
||||||
click.echo(f'Error: Command returned error code {exit_code}.', err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def verify_absence_of(file_path):
|
|
||||||
if m4b_path.exists():
|
|
||||||
click.echo(f"Error: {file_path} must be moved out of the way or deleted.", err=True)
|
|
||||||
sys.exit(1)
|
|
73
readme.md
73
readme.md
|
@ -1,54 +1,20 @@
|
||||||
# Knock
|
# Knock
|
||||||
|
|
||||||
Perform the following conversions with one command:
|
Convert ACSM files to PDF/EPUBs with one command on Linux ([and MacOS very soon](https://github.com/BentonEdmondson/knock/issues/58)).
|
||||||
* ACSM → EPUB
|
|
||||||
* ACSM → PDF
|
|
||||||
* (Soon: AAX → M4B)
|
|
||||||
|
|
||||||
![CLI demonstration](demo.png)
|
|
||||||
|
|
||||||
*This software does not utilize Adobe Digital Editions nor Wine. It is completely free and open-source software written natively for Linux.*
|
*This software does not utilize Adobe Digital Editions nor Wine. It is completely free and open-source software written natively for Linux.*
|
||||||
|
|
||||||
## Setup and Installation
|
## Installation
|
||||||
|
|
||||||
* For NixOS users, include this flake in your system `flake.nix`. Then run `knock ~/path/to/my-book.acsm` to use.
|
* Download the latest [release](https://github.com/BentonEdmondson/knock/releases). Make sure it is the correct version for your architecture (run `uname -m` to check).
|
||||||
```nix
|
* Rename the binary and make it executable.
|
||||||
{
|
* Run `knock /path/to/book.acsm` to perform the conversion.
|
||||||
inputs.knock.url = "github:BentonEdmondson/knock";
|
|
||||||
outputs = { self, knock }: { /* knock.defaultPackage.x86_64-linux is the package */ };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
* For non-NixOS, use the latest [release](https://github.com/BentonEdmondson/knock/releases). It is large because it includes all dependencies, allowing it to run on any system with an x86_64 Linux kernel. It was built using [`nix bundle`](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-bundle.html). Use it by doing the following:
|
|
||||||
1. Download `knock-version-x86_64-linux` and open a terminal
|
|
||||||
1. Navigate to the folder within which `knock-version-x86_64-linux` resides (e.g. `cd ~/Downloads`)
|
|
||||||
1. Run `mv knock-version-x86_64-linux knock` to rename it to `knock`
|
|
||||||
1. Run `chmod +x knock` to make it executable
|
|
||||||
1. Run `./knock ~/path/to/my-book.acsm` to convert the ebook
|
|
||||||
|
|
||||||
If you receive an error that says something like `./nix/store/...: not found` or `./nix/store/...: No such file or directory` then you might not have user namespaces enabled. Try running the following to fix it:
|
|
||||||
|
|
||||||
```
|
|
||||||
echo "kernel.unprivileged_userns_clone=1" >> /etc/sysctl.conf
|
|
||||||
sudo reboot
|
|
||||||
```
|
|
||||||
If you receive an error that says something like `E_AUTH_FAILED http://adeactivate.adobe.com/adept/SignInDirect xxxx@xxxxxxxx.com CUS05051` then you might have over (at least) 10 digit password for Adobe. Try changing it to 10 digit password and try the command again.
|
|
||||||
|
|
||||||
1. Optionally move the executable to `~/bin` (for your user) or `/usr/local/bin/` (for all users) to allow it to run from anywhere (might not work on some distributions)
|
|
||||||
|
|
||||||
## Recommended Workflows
|
|
||||||
|
|
||||||
Before buying your ebook/audiobook, check if it is available for free on [Project Gutenberg](https://gutenberg.org/) (ebooks) or [LibriVox](https://librivox.org/) (audiobooks).
|
|
||||||
|
|
||||||
If you're looking for an ebook reader or audiobook player, I recommend [Foliate](https://johnfactotum.github.io/foliate/) for the former and [Cozy](https://cozy.sh/) for the latter.
|
|
||||||
|
|
||||||
## Verified Book Sources
|
## Verified Book Sources
|
||||||
|
|
||||||
Knock should work on any ACSM file, but it has been specifically verified to work on ACSM files from the following:
|
Knock should work on any ACSM file, but it has been specifically verified to work on ACSM files purchased [eBooks.com](https://www.ebooks.com/en-us/) and [Kobo](https://www.kobo.com/us/en), among others.
|
||||||
|
|
||||||
* [eBooks.com](https://www.ebooks.com/en-us/)
|
Before buying your ebook, check if it is available for free on [Project Gutenberg](https://gutenberg.org/).
|
||||||
* [Rakuten Kobo](https://www.kobo.com/us/en)
|
|
||||||
* [Google Books](https://books.google.com/)
|
|
||||||
* [Hugendubel.de](https://www.hugendubel.de/de/) (German)
|
|
||||||
|
|
||||||
## The Name
|
## The Name
|
||||||
|
|
||||||
|
@ -65,13 +31,26 @@ The name comes from the [D&D 5e spell](https://roll20.net/compendium/dnd5e/Knock
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
* [`libgourou`](http://indefero.soutade.fr/p/libgourou/) for using the ACSM file to download the corresponding encrypted EPUB/PDF file from Adobe's servers
|
There are no userspace runtime dependencies.
|
||||||
* [`rmdrm`](https://github.com/BentonEdmondson/rmdrm/) for decrypting the Adobe ADEPT-encrypted EPUB/PDF files
|
|
||||||
* [`Audible`](https://github.com/mkb79/Audible) for fetching the Audible decryption key used to decrypt AAX files
|
|
||||||
* [`ffmpeg`](https://www.ffmpeg.org/) for converting AAX files to M4B files using the Audible decryption key
|
|
||||||
|
|
||||||
These are already included in all releases and in the Nix flake of course.
|
## Building & Contributing
|
||||||
|
|
||||||
|
Install [Nix](https://github.com/NixOS/nix) if you don't have it. [Enable flakes](https://nixos.wiki/wiki/Flakes) if you haven't. Run
|
||||||
|
|
||||||
|
```
|
||||||
|
nix build
|
||||||
|
```
|
||||||
|
|
||||||
|
to build and
|
||||||
|
|
||||||
|
```
|
||||||
|
nix flake update
|
||||||
|
```
|
||||||
|
|
||||||
|
to update libraries.
|
||||||
|
|
||||||
|
Test books can be found [here](https://www.adobe.com/solutions/ebook/digital-editions/sample-ebook-library.html).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This software is licensed under GPLv3.
|
This software is licensed under GPLv3. The linked libraries have various licenses.
|
||||||
|
|
104
src/knock.cpp
Normal file
104
src/knock.cpp
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
#include <filesystem>
|
||||||
|
#include "drmprocessorclientimpl.h"
|
||||||
|
#include "libgourou_common.h"
|
||||||
|
#include "libgourou.h"
|
||||||
|
|
||||||
|
std::string get_data_dir();
|
||||||
|
void verify_absence(std::string file);
|
||||||
|
void verify_presence(std::string file);
|
||||||
|
|
||||||
|
int main(int argc, char** argv) try {
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
throw std::invalid_argument("the ACSM file must be passed as an argument");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string acsm_file = argv[1];
|
||||||
|
verify_presence(acsm_file);
|
||||||
|
const std::string acsm_stem = acsm_file.substr(0, acsm_file.find_last_of("."));
|
||||||
|
const std::string drm_file = acsm_stem + ".drm";
|
||||||
|
const std::string out_file = acsm_stem + ".out";
|
||||||
|
verify_absence(drm_file);
|
||||||
|
verify_absence(out_file);
|
||||||
|
const std::string knock_data = get_data_dir();
|
||||||
|
|
||||||
|
DRMProcessorClientImpl client;
|
||||||
|
gourou::DRMProcessor* processor = gourou::DRMProcessor::createDRMProcessor(
|
||||||
|
&client,
|
||||||
|
false, // don't "always generate a new device" (default)
|
||||||
|
knock_data
|
||||||
|
);
|
||||||
|
|
||||||
|
processor->signIn("anonymous", "");
|
||||||
|
processor->activateDevice();
|
||||||
|
|
||||||
|
std::cout << "downloading the file from Adobe..." << std::endl;
|
||||||
|
gourou::FulfillmentItem* item = processor->fulfill(acsm_file);
|
||||||
|
gourou::DRMProcessor::ITEM_TYPE type = processor->download(item, drm_file);
|
||||||
|
|
||||||
|
std::cout << "removing DRM from the file..." << std::endl;
|
||||||
|
std::string ext_file;
|
||||||
|
std::string file_type;
|
||||||
|
switch (type) {
|
||||||
|
case gourou::DRMProcessor::ITEM_TYPE::PDF: {
|
||||||
|
// for pdfs the function moves the pdf while removing drm
|
||||||
|
processor->removeDRM(drm_file, out_file, type, nullptr, 0);
|
||||||
|
std::filesystem::remove(drm_file);
|
||||||
|
ext_file = acsm_stem + ".pdf";
|
||||||
|
file_type = "PDF";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case gourou::DRMProcessor::ITEM_TYPE::EPUB: {
|
||||||
|
// for epubs the drm is removed in-place so in == out
|
||||||
|
processor->removeDRM(drm_file, drm_file, type, nullptr, 0);
|
||||||
|
std::filesystem::rename(drm_file, out_file);
|
||||||
|
ext_file = acsm_stem + ".epub";
|
||||||
|
file_type = "EPUB";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::filesystem::exists(ext_file)) {
|
||||||
|
std::cerr
|
||||||
|
<< "warning: failed to update file extension; " + ext_file + " already exists"
|
||||||
|
<< std::endl;
|
||||||
|
ext_file = out_file;
|
||||||
|
} else {
|
||||||
|
std::filesystem::rename(out_file, ext_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "DRM-free " + file_type + " file generated at " + ext_file << std::endl;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
} catch (const gourou::Exception& e) {
|
||||||
|
std::cerr << "error:\n" << e.what();
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "error: " << e.what() << std::endl;
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string get_data_dir() {
|
||||||
|
char* xdg_data_home = std::getenv("XDG_DATA_HOME");
|
||||||
|
std::string knock_data;
|
||||||
|
if (xdg_data_home != nullptr) {
|
||||||
|
knock_data = xdg_data_home;
|
||||||
|
} else {
|
||||||
|
knock_data = std::string(std::getenv("HOME")) + "/.local/share";
|
||||||
|
}
|
||||||
|
knock_data += "/knock/acsm";
|
||||||
|
return knock_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void verify_absence(std::string file) {
|
||||||
|
if (std::filesystem::exists(file)) {
|
||||||
|
throw std::runtime_error("file " + file + " must be moved out of the way or deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void verify_presence(std::string file) {
|
||||||
|
if (!std::filesystem::exists(file)) {
|
||||||
|
throw std::runtime_error("file " + file + " does not exist");
|
||||||
|
}
|
||||||
|
}
|
46
src/knock.py
46
src/knock.py
|
@ -1,46 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import subprocess, shutil, click
|
|
||||||
from pathlib import Path
|
|
||||||
from getpass import getpass
|
|
||||||
from xdg import xdg_config_home
|
|
||||||
|
|
||||||
from handle_acsm import handle_acsm
|
|
||||||
from handle_aax import handle_aax
|
|
||||||
|
|
||||||
@click.command()
|
|
||||||
@click.argument(
|
|
||||||
"path",
|
|
||||||
type=click.Path(
|
|
||||||
exists=True,
|
|
||||||
file_okay=True,
|
|
||||||
dir_okay=False,
|
|
||||||
readable=True,
|
|
||||||
resolve_path=True
|
|
||||||
)
|
|
||||||
)
|
|
||||||
def main(path):
|
|
||||||
path = Path(path)
|
|
||||||
|
|
||||||
# make the config dir if it doesn't exist
|
|
||||||
xdg_config_home().joinpath('knock').mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
path_type = path.suffix[1:].upper()
|
|
||||||
|
|
||||||
if path_type == 'ACSM':
|
|
||||||
click.echo('Received an ACSM (Adobe) 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 {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', err=True)
|
|
||||||
#click.echo(' * AAX (Audible)\n', 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{path_type}%20Files&labels=enhancement', err=True)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
Loading…
Reference in a new issue