Skip to content

Commit

Permalink
Initial work on issue #6. Most interactions with the audible CLI now …
Browse files Browse the repository at this point in the history
…result in whether or not the CLI command was successful. Added a function to check authentication.
  • Loading branch information
knicholson32 committed Dec 25, 2023
1 parent f3d8179 commit f8841d4
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 50 deletions.
3 changes: 3 additions & 0 deletions src/lib/server/cmd/audible/cmd/download/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export const download = async (
if (data.indexOf('audible.exceptions.NetworkError') !== -1) {
global.audible.cancelMap[asin].error = BookDownloadError.NETWORK_ERROR;
audible.kill();
} else if (data.indexOf('audible.exceptions.Unauthorized: Forbidden (403)') !== -1) {
global.audible.cancelMap[asin].error = BookDownloadError.NOT_AUTHORIZED;
audible.kill();
}
};

Expand Down
111 changes: 90 additions & 21 deletions src/lib/server/cmd/audible/cmd/library/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as child_process from 'node:child_process';
import * as fs from 'fs';
import { v4 as uuidv4 } from 'uuid';
import type { Library, BookFromCLI } from '../../types';
import { type Library, type BookFromCLI, type BookDownloadError, CLIError } from '../../types';
import { Event, SourceType } from '$lib/types';
import { isLocked } from '../../';
import prisma from '$lib/server/prisma';
Expand Down Expand Up @@ -300,15 +300,65 @@ const processBook = async (book: BookFromCLI, id: string): Promise<boolean> => {
return true;
};

/**
* Check that an Audible profile is authenticated
* @param id the ID of the source to check
* @returns whether or not it is authenticated, or null if unknown
*/
export const checkAuthenticated = async (id: string): Promise<boolean | null> => {
// Check that the ID was actually submitted
if (id === null || id === undefined) return null;

// Get the profile from the database
const source = await prisma.source.findUnique({
where: {
type: SourceType.AUDIBLE,
NOT: { audible: null },
id
},
include: { audible: true }
});

// Return if the profile was not found
if (source === null || source === undefined || source.audible === null || isLocked()) return null;

// Make sure the config file is written
await writeConfigFile();

events.emitProgress('basic.account.sync', id, {
t: Event.Progress.Basic.Stage.START
});

const cli_id = source.audible.cli_id;
try {
await new Promise<void>((resolve, reject) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${cli_id} api /1.0/account/information`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
(err, stdout) => {
if (err !== null) reject(stdout);
else resolve()
}
);
});
return true;
} catch (e) {
const err = e as string;
console.log('ERR internal error, not authenticated');
console.log(e);
return false;
}
}

/**
* Parse a library JSON file and import all the books to the DB
* @param id the account to associate the book with
*/
export const get = async (
id: string
): Promise<{ numCreated: number; numUpdated: number } | null> => {
): Promise<{ err: CLIError, results?: { numCreated: number; numUpdated: number }}> => {
// Check that the ID was actually submitted
if (id === null || id === undefined) return null;
if (id === null || id === undefined) return { err: CLIError.NO_ID };

// Get the profile from the database
const source = await prisma.source.findUnique({
Expand All @@ -321,7 +371,7 @@ export const get = async (
});

// Return if the profile was not found
if (source === null || source === undefined || source.audible === null || isLocked()) return null;
if (source === null || source === undefined || source.audible === null || isLocked()) return { err: CLIError.NO_SOURCE };

// Create a temp directory for this library
if (!fs.existsSync(`/tmp`)) fs.mkdirSync(`/tmp`);
Expand All @@ -336,20 +386,39 @@ export const get = async (
try {
const cli_id = source.audible.cli_id;

await new Promise<void>((resolve) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${cli_id} library export --format json -o /tmp/${cli_id}.library.json`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
() => resolve()
);
});
await new Promise<void>((resolve) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${cli_id} library export --format tsv -o /db/audible/${cli_id}.library.tsv`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
() => resolve()
);
});
try {
await new Promise<void>((resolve, reject) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${cli_id} library export --format json -o /tmp/${cli_id}.library.json`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
(err, stdout) => {
if (err !== null) reject(stdout);
else resolve()
}
);
});
await new Promise<void>((resolve, reject) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${cli_id} library export --format tsv -o /db/audible/${cli_id}.library.tsv`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
(err, stdout) => {
if (err !== null) reject(stdout);
else resolve()
}
);
});
} catch (e) {
const err = e as string;
console.log('ERR internal error');
console.log(e);
if (err.indexOf('audible.exceptions.Unauthorized: Forbidden (403)') !== -1) {
return { err: CLIError.NOT_AUTHORIZED };
} else if (err.indexOf('audible.exceptions.NetworkError: Network down.') !== -1) {
return { err: CLIError.NETWORK_ERROR };
} else {
return { err: 'UNKNOWN' as CLIError };
}
}

const library = JSON.parse(
fs.readFileSync(`/tmp/${cli_id}.library.json`).toString()
Expand Down Expand Up @@ -386,10 +455,10 @@ export const get = async (
success: true
});

return { numCreated, numUpdated };
return { err: CLIError.NO_ERROR, results: { numCreated, numUpdated } };
} catch (e) {
// Didn't work
console.log('ERR');
console.log('ERR', e);
const err = e as { stdout: Buffer };
console.log(err);
console.log(err.stdout.toString());
Expand All @@ -402,6 +471,6 @@ export const get = async (
try {
fs.rmSync(`/tmp/${id}.library.json`, { recursive: true, force: true });
} catch (e) {}
return null;
return { err: 'UNKNOWN' as CLIError };
}
};
7 changes: 5 additions & 2 deletions src/lib/server/cmd/audible/cmd/profile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,14 @@ export const fetchMetadata = async (

try {
// Have the audible-cli get the activation bytes
await new Promise<void>((resolve) => {
await new Promise<void>((resolve, reject) => {
child_process.exec(
`${AUDIBLE_CMD} -P ${source.audible?.cli_id} activation-bytes`,
{ env: { AUDIBLE_CONFIG_DIR: AUDIBLE_FOLDER } },
() => resolve()
(err, stdout) => {
if (err !== null) reject(stdout);
else resolve()
}
);
});
// Get the auth file associated with this profile
Expand Down
32 changes: 32 additions & 0 deletions src/lib/server/cmd/audible/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export enum BookDownloadError {
BOOK_NOT_FOUND = 'BOOK_NOT_FOUND',
CANCELED = 'CANCELED',
NETWORK_ERROR = 'NETWORK_ERROR',
NOT_AUTHORIZED = 'NOT_AUTHORIZED',
NO_PROFILE = 'NO_PROFILE'
}

Expand All @@ -183,9 +184,40 @@ export const bookDownloadErrorToString = (e: BookDownloadError): string => {
return 'The book download process was canceled';
case BookDownloadError.NETWORK_ERROR:
return 'A network issue exists that is preventing download';
case BookDownloadError.NOT_AUTHORIZED:
return 'This account is not authorized by Audible';
case BookDownloadError.NO_PROFILE:
return 'No profile exists to download this book';
default:
return 'An unknown error occurred';
}
};


export enum CLIError {
NO_ERROR = 'NO_ERROR',
AUDIBLE_LOCKED = 'AUDIBLE_LOCKED',
NO_ID = 'NO_ID',
NO_SOURCE = 'NO_SOURCE',
NETWORK_ERROR = 'NETWORK_ERROR',
NOT_AUTHORIZED = 'NOT_AUTHORIZED',
}

export const cliErrorToString = (e: CLIError): string => {
switch (e) {
case CLIError.NO_ERROR:
return 'No error';
case CLIError.AUDIBLE_LOCKED:
return 'The audible CLI is locked';
case CLIError.NO_ID:
return 'No ID was submitted';
case CLIError.NO_SOURCE:
return 'No Audible source provided';
case CLIError.NETWORK_ERROR:
return 'There was a network error';
case CLIError.NOT_AUTHORIZED:
return 'This account is not authorized by Audible';
default:
return 'An unknown error occurred';
}
};
12 changes: 6 additions & 6 deletions src/lib/server/cmd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as tools from './tools';
import * as settings from '$lib/server/settings';
import * as types from '$lib/types';
import { v4 as uuidv4 } from 'uuid';
import { BookDownloadError } from './audible/types';
import { BookDownloadError, CLIError, cliErrorToString } from './audible/types';
import type { Issuer, ModalTheme } from '$lib/types';
import { ConversionError } from './AAXtoMP3/types';
import { ProcessError } from '$lib/types';
Expand Down Expand Up @@ -921,16 +921,16 @@ export namespace Cron {
// Sync the profile
const results = await audible.cmd.library.get(source.id);

if (results !== null) {
if (results.err === CLIError.NO_ERROR && results.results !== undefined) {
cronRecord.libSync++;
cronRecord.booksAdded += results.numCreated;
cronRecord.booksUpdated += results.numUpdated;
cronRecord.booksAdded += results.results.numCreated;
cronRecord.booksUpdated += results.results.numUpdated;
}

// Check if the sync worked
if (debug) {
if (results !== null) console.log(JSON.stringify(results));
else console.log('Sync failed');
if (results.err === CLIError.NO_ERROR) console.log(JSON.stringify(results));
else console.log('Sync failed: ', cliErrorToString(results.err));
}
} else {
if (debug)
Expand Down
7 changes: 6 additions & 1 deletion src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ export enum ProcessError {
DESTINATION_NOT_WRITABLE = 'DESTINATION_NOT_WRITABLE',
INVALID_FILE = 'INVALID_FILE',
CONVERSION_ERROR = 'CONVERSION_ERROR',
COULD_NOT_SAVE = 'COULD_NOT_SAVE'
COULD_NOT_SAVE = 'COULD_NOT_SAVE',
NOT_AUTHORIZED = 'NOT_AUTHORIZED',
}

export const processErrorToStringShort = (p: ProcessError) => {
Expand Down Expand Up @@ -254,6 +255,8 @@ export const processErrorToStringShort = (p: ProcessError) => {
return 'Conversion Error';
case ProcessError.COULD_NOT_SAVE:
return 'Save Error';
case ProcessError.NOT_AUTHORIZED:
return 'Not Authorized';
default:
return 'Unknown error';
}
Expand Down Expand Up @@ -285,6 +288,8 @@ export const processErrorToStringLong = (p: ProcessError) => {
return "Something went wrong while converting this book's audio file.";
case ProcessError.COULD_NOT_SAVE:
return "Something went wrong while copying this book's audio file to storage.";
case ProcessError.NOT_AUTHORIZED:
return "This account source is not authorized. Please sign-in again.";
default:
return 'An unknown and unexpected error has occurred.';
}
Expand Down
Loading

0 comments on commit f8841d4

Please sign in to comment.