Skip to content

Commit 862f0e3

Browse files
committed
Remove ffmpeg-python and use subprocess exclusively for decryption
The ffmpeg-python library has a known bug that prevents map_metadata and map_chapters from working correctly. Since the fix hasn't been merged in over 2 years, we're switching to subprocess exclusively. ## Issue ffmpeg-python doesn't properly handle -map_metadata and -map_chapters parameters. When passed as kwargs, they don't get translated to the correct FFmpeg command-line arguments. Issue: kkroening/ffmpeg-python#463 PR (unmerged): kkroening/ffmpeg-python#814 ## Changes ### Simplified decrypt_aaxc() - Removed ffmpeg-python implementation attempt - Removed fallback pattern (_decrypt_aaxc_subprocess helper) - Now uses subprocess directly for all decryption - Added comment explaining why we don't use ffmpeg-python - Cleaner, simpler code with no hybrid approach needed ### Updated imports - Removed ffmpeg import from src/downloader.py - Removed ffmpeg-python from requirements.txt ## Benefits 1. **Reliable**: Subprocess approach is proven to work 2. **Simpler**: No fallback logic, no hybrid approach 3. **Maintainable**: Direct FFmpeg command construction is easier to debug 4. **Cleaner dependencies**: One less package to maintain The subprocess implementation gives us precise control over the FFmpeg command and handles all our use cases correctly (metadata, chapters, cover art embedding).
1 parent b5d203b commit 862f0e3

2 files changed

Lines changed: 2 additions & 72 deletions

File tree

requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ click==8.1.8
77
click-default-group==1.2.4
88
exceptiongroup==1.2.2
99
fastapi==0.115.12
10-
ffmpeg-python==0.2.0
1110
future==1.0.0
1211
h11==0.14.0
1312
httpcore==1.0.7

src/downloader.py

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from pathlib import Path
55
import shutil
66
import httpx
7-
import ffmpeg
87
from tqdm import tqdm
98
from src.audible import Audible
109
from audible.aescipher import decrypt_voucher_from_licenserequest
@@ -355,76 +354,9 @@ def decrypt_aaxc(book: str, voucher: str, book_data: tuple = None, cover_path: s
355354
else:
356355
logger.info("Metadata file created: %s", metadata_file)
357356

358-
# Build ffmpeg command using ffmpeg-python library
359-
# Create all inputs
360-
audio = ffmpeg.input(book, audible_key=key, audible_iv=iv)
361-
362-
inputs = [audio]
363-
output_opts = {
364-
'map': '0:a',
365-
'c:a': 'copy',
366-
'dn': None,
367-
'loglevel': 'warning',
368-
'y': None
369-
}
370-
371-
# Add cover if provided
372-
if cover_path and Path(cover_path).exists():
373-
cover = ffmpeg.input(cover_path)
374-
inputs.append(cover)
375-
# When we have multiple streams to map, we need to use multiple map options
376-
output_opts['map'] = ['0:a', '1:v']
377-
output_opts['c:v'] = 'copy'
378-
output_opts['disposition:v'] = 'attached_pic'
379-
logger.info("Embedding cover art from: %s", cover_path)
380-
381-
# Add metadata file if generated (includes chapters if provided)
382-
if metadata_file and Path(metadata_file).exists():
383-
metadata_input = ffmpeg.input(metadata_file, f='ffmetadata')
384-
inputs.append(metadata_input)
385-
# Metadata index is based on number of inputs added so far
386-
metadata_idx = len(inputs) - 1
387-
output_opts['map_metadata'] = str(metadata_idx)
388-
# Chapters are embedded in the same metadata file
389-
output_opts['map_chapters'] = str(metadata_idx)
390-
391-
# Create output with all inputs
392-
stream = ffmpeg.output(*inputs, output_file, **output_opts)
393-
394-
# Run the command
395-
try:
396-
ffmpeg.run(stream, capture_stdout=True, capture_stderr=True)
397-
except ffmpeg.Error as e:
398-
error_msg = e.stderr.decode() if e.stderr else str(e)
399-
logger.error("FFmpeg error: %s", error_msg)
400-
401-
# If ffmpeg-python fails, fall back to subprocess approach
402-
logger.warning("Falling back to subprocess implementation")
403-
return _decrypt_aaxc_subprocess(book, voucher, key, iv, metadata_file, cover_path, output_file, chapters)
404-
405-
# Cleanup temporary metadata file
406-
if metadata_file and Path(metadata_file).exists():
407-
Path(metadata_file).unlink()
408-
logger.debug("Cleaned up metadata file: %s", metadata_file)
409-
410-
logger.info("Conversion complete: %s", output_file)
411-
return output_file
412-
413-
414-
def _decrypt_aaxc_subprocess(book: str, voucher: str, key: str, iv: str,
415-
metadata_file: str = None, cover_path: str = None,
416-
output_file: str = None, chapters: list = None):
417-
"""
418-
Fallback subprocess implementation for FFmpeg decryption.
419-
420-
Used when ffmpeg-python fails to handle complex multi-input scenarios.
421-
Note: chapters parameter is unused here as chapters are already embedded
422-
in the metadata_file when it's created.
423-
"""
424-
if output_file is None:
425-
output_file = f"{book}.m4b"
426-
427357
# Build ffmpeg command using subprocess for precise control
358+
# Note: ffmpeg-python library has issues with map_metadata/map_chapters
359+
# See: https://github.com/kkroening/ffmpeg-python/issues/463
428360
cmd = ['ffmpeg', '-y']
429361

430362
# Add decryption keys and primary audio input
@@ -452,7 +384,6 @@ def _decrypt_aaxc_subprocess(book: str, voucher: str, key: str, iv: str,
452384
# Input 0: audio (AAXC)
453385
# Input 1: cover (if present)
454386
# Input 2 or 1: metadata file (if present, includes chapters)
455-
456387
input_idx = 1
457388
if cover_path and Path(cover_path).exists():
458389
input_idx += 1

0 commit comments

Comments
 (0)