Skip to content

Commit bbd30ea

Browse files
committed
chore: add screenshot
1 parent a2f3f34 commit bbd30ea

File tree

4 files changed

+69
-28
lines changed

4 files changed

+69
-28
lines changed

.github/art/screenshot.png

45.2 KB
Loading

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
A TypeScript library and CLI tool for converting Audible AAX audiobooks to standard MP3, M4A, or M4B formats.
1212

13+
![Screenshot](.github/art/screenshot.png)
14+
1315
## Features
1416

1517
- Convert AAX files to MP3, M4A, or M4B formats

src/converter.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { ConversionOptions, ConversionResult } from './types'
22
import { existsSync, mkdirSync } from 'node:fs'
33
import path from 'node:path'
44
import { config } from './config'
5+
import { getActivationBytesFromAudibleCli } from './utils/activation'
56
import { checkFFmpeg, runFFmpeg } from './utils/ffmpeg'
67
import { logger } from './utils/logger'
78
import { getBookMetadata } from './utils/metadata'
@@ -51,6 +52,14 @@ function generateOutputPath(metadata: any, options: ConversionOptions): string {
5152
*/
5253
export async function convertAAX(options: ConversionOptions): Promise<ConversionResult> {
5354
// Validate input file
55+
if (!options.inputFile) {
56+
logger.error('No input file provided. Please specify an AAX file to convert.')
57+
return {
58+
success: false,
59+
error: 'No input file provided',
60+
}
61+
}
62+
5463
if (!existsSync(options.inputFile)) {
5564
logger.error(`Input file does not exist: ${options.inputFile}`)
5665
return {
@@ -59,24 +68,24 @@ export async function convertAAX(options: ConversionOptions): Promise<Conversion
5968
}
6069
}
6170

62-
// Start the conversion process with a nice header
63-
await logger.box(`Converting ${path.basename(options.inputFile)}`)
64-
65-
// Check FFmpeg
66-
logger.info('Checking FFmpeg installation...')
71+
// Check for ffmpeg
6772
const ffmpegAvailable = await checkFFmpeg()
6873
if (!ffmpegAvailable) {
69-
logger.error('FFmpeg is not available. Please install FFmpeg or set ffmpegPath in config.')
74+
logger.error('FFmpeg is not available. Please install FFmpeg and try again.')
7075
return {
7176
success: false,
72-
error: 'FFmpeg is not available. Please install FFmpeg or set ffmpegPath in config.',
77+
error: 'FFmpeg not found. Required for conversion.',
7378
}
7479
}
80+
logger.info('Checking FFmpeg installation...')
81+
82+
// Get activation code
83+
const activationCode = options.activationCode || config.activationCode || await getActivationBytesFromAudibleCli()
7584

76-
// Validate activation code
77-
const activationCode = options.activationCode || config.activationCode
7885
if (!activationCode) {
79-
logger.error('No activation code provided. This is required to convert AAX files.')
86+
logger.error('No activation code provided for decryption.')
87+
logger.info('You can specify an activation code with the -c option or set it in your config.')
88+
logger.info('To auto-detect activation code, run "aax setup-audible" first.')
8089
return {
8190
success: false,
8291
error: 'No activation code provided. This is required to convert AAX files.',
@@ -113,8 +122,13 @@ export async function convertAAX(options: ConversionOptions): Promise<Conversion
113122
logger.info(`Chapters: ${metadata.chapters.length}`)
114123
}
115124

125+
// Avoid duplicate logs by using logger.debug for detailed path info
116126
logger.info(`Output format: ${options.outputFormat || config.outputFormat || 'mp3'}`)
117-
logger.info(`Output path: ${outputPath}`)
127+
128+
// Only log the base output path once, more detailed path info in debug
129+
const shortPath = path.basename(outputPath)
130+
logger.info(`Output path: ${shortPath}`)
131+
logger.debug(`Full output path: ${outputPath}`)
118132

119133
// Build FFmpeg command
120134
logger.info('Preparing FFmpeg command...')

src/utils/ffmpeg.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,22 +81,23 @@ export async function runFFmpeg(args: string[]): Promise<{ success: boolean, out
8181
let currentSize = '0KB'
8282
let currentSpeed = '0x'
8383

84-
// Progress bar
85-
const progressBar = isMetadataExtraction
86-
? null
87-
: logger.progress(100, 'Preparing conversion...')
84+
// Progress bar reference - only created once explicitly needed
85+
let progressBar: ReturnType<typeof logger.progress> | null = null
86+
87+
// Flag to prevent duplicate completion messages
88+
let progressCompleted = false
8889

8990
// Regular update interval
9091
const updateInterval = setInterval(() => {
91-
if (!progressBar || !bookDuration)
92+
if (!progressBar || !bookDuration || progressCompleted)
9293
return
9394

9495
// Only apply time-based updates if we're actually making progress
9596
const now = Date.now()
9697
if (now - lastOutputTime > 3000 && currentTimeMs > 0) {
9798
// It's been a while since the last FFmpeg update, show progress is still happening
9899
// Assume some minimal progress
99-
const progressPercent = Math.max(0.1, Math.min(99.9, (currentTimeMs / bookDuration) * 100))
100+
const progressPercent = Math.min(99, (currentTimeMs / bookDuration) * 100)
100101
progressBar.update(progressPercent, `Converting ${formatTime(currentTimeMs)} / ${formatTime(bookDuration)} (${currentSpeed}) - Size: ${currentSize}`)
101102

102103
// Small time progress so user knows it's not frozen
@@ -105,24 +106,31 @@ export async function runFFmpeg(args: string[]): Promise<{ success: boolean, out
105106
}, 1000)
106107

107108
function updateProgressFromOutput(text: string) {
108-
if (!progressBar)
109-
return
110-
111109
const progress = parseFFmpegProgress(text)
112110

113111
// Get total duration once we know it
114112
if (progress.totalMs && progress.totalMs > 0 && !bookDuration) {
115113
bookDuration = progress.totalMs
114+
115+
// Create progress bar when we first know the duration
116+
// This prevents creating it prematurely
117+
if (!isMetadataExtraction && !progressBar && !progressCompleted) {
118+
progressBar = logger.progress(100, 'Starting conversion...')
119+
}
116120
}
117121

122+
// Skip further processing if no progress bar
123+
if (!progressBar || progressCompleted)
124+
return
125+
118126
// Update time position if provided
119127
if (progress.timeMs !== undefined) {
120128
currentTimeMs = progress.timeMs
121129
lastOutputTime = Date.now()
122130

123131
// Calculate progress percentage based on time
124132
if (bookDuration) {
125-
const progressPercent = Math.max(0.1, Math.min(99.9, (currentTimeMs / bookDuration) * 100))
133+
const progressPercent = Math.min(99, (currentTimeMs / bookDuration) * 100)
126134

127135
// Update size and speed if available
128136
if (progress.size)
@@ -191,19 +199,31 @@ export async function runFFmpeg(args: string[]): Promise<{ success: boolean, out
191199
ffmpeg.on('close', (code) => {
192200
clearInterval(updateInterval)
193201

194-
if (progressBar) {
202+
if (progressBar && !progressCompleted) {
203+
progressCompleted = true
204+
195205
if (code === 0) {
196-
progressBar.finish('Conversion completed successfully!')
206+
// This is where the duplicate message happens
207+
// Set to 100% first and only then finish with message
208+
progressBar.update(100)
209+
const barRef = progressBar // Store reference to avoid null issues
210+
setTimeout(() => {
211+
if (barRef)
212+
barRef.finish('Conversion completed successfully!')
213+
}, 50)
197214
}
198215
else {
199216
progressBar.interrupt('Conversion failed', 'error')
200217
}
201218
}
202219

203-
resolve({
204-
success: code === 0,
205-
output,
206-
})
220+
// Give a small delay to allow the progress bar to finish properly
221+
setTimeout(() => {
222+
resolve({
223+
success: code === 0,
224+
output,
225+
})
226+
}, 100)
207227
})
208228
})
209229
}
@@ -212,7 +232,12 @@ export async function runFFmpeg(args: string[]): Promise<{ success: boolean, out
212232
* Extracts metadata from an AAX file using FFmpeg
213233
*/
214234
export async function extractAAXMetadata(filePath: string): Promise<string> {
215-
logger.info(`Extracting metadata from ${filePath}...`)
235+
// Shorten the filepath display if it's too long
236+
const displayPath = filePath.length > 70
237+
? `...${filePath.substring(filePath.length - 67)}`
238+
: filePath
239+
240+
logger.debug(`Extracting metadata from ${displayPath}...`)
216241

217242
// For metadata extraction we use a simple info message rather than a progress bar
218243
// since it's usually quick and doesn't provide meaningful progress information

0 commit comments

Comments
 (0)