397 lines
12 KiB
Bash
Executable File
397 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Media Library FLAC to Opus Conversion Script
|
|
# Processes multiple albums in a media library, converting all FLAC files to Opus 448kbps
|
|
# Also copies cover art files to maintain album artwork
|
|
|
|
# Check if ffmpeg is installed
|
|
if ! command -v ffmpeg &> /dev/null; then
|
|
echo "Error: ffmpeg is not installed or not in PATH"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if ffmpeg has opus encoder support
|
|
if ! ffmpeg -encoders 2>/dev/null | grep -q "libopus"; then
|
|
echo "Error: ffmpeg does not have Opus encoder support (libopus)"
|
|
echo "Please install ffmpeg with Opus support"
|
|
exit 1
|
|
fi
|
|
|
|
# Function to display usage
|
|
show_usage() {
|
|
echo "Usage: $0 [OPTIONS] <media_library_path>"
|
|
echo ""
|
|
echo "Options:"
|
|
echo " -o, --output DIR Output directory (default: creates _opus suffix)"
|
|
echo " -r, --replace Replace original files with Opus (use with caution)"
|
|
echo " -p, --preserve Create Opus files alongside original FLAC files"
|
|
echo " -t, --test Test mode - show what would be processed"
|
|
echo " -f, --force Force re-conversion of existing Opus files"
|
|
echo " -b, --bitrate RATE Opus bitrate in kbps (default: 448)"
|
|
echo " --no-covers Skip copying cover art files"
|
|
echo " -h, --help Show this help message"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 /path/to/music/library"
|
|
echo " $0 -o /path/to/output /path/to/music/library"
|
|
echo " $0 --test /path/to/music/library"
|
|
echo " $0 --replace /path/to/music/library"
|
|
echo " $0 -b 384 /path/to/music/library"
|
|
echo " $0 --no-covers /path/to/music/library"
|
|
}
|
|
|
|
# Default options
|
|
OUTPUT_DIR=""
|
|
REPLACE_MODE=false
|
|
PRESERVE_MODE=false
|
|
TEST_MODE=false
|
|
FORCE_MODE=false
|
|
COPY_COVERS=true
|
|
OPUS_BITRATE=192
|
|
PROCESSED_COUNT=0
|
|
SKIPPED_COUNT=0
|
|
ERROR_COUNT=0
|
|
COVERS_COPIED=0
|
|
COVERS_SKIPPED=0
|
|
|
|
# Common cover art filenames to look for
|
|
COVER_FILENAMES=("cover.jpg" "Cover.jpg" "COVER.jpg" "folder.jpg" "Folder.jpg" "FOLDER.jpg"
|
|
"album.jpg" "Album.jpg" "ALBUM.jpg" "front.jpg" "Front.jpg" "FRONT.jpg"
|
|
"cover.jpeg" "Cover.jpeg" "COVER.jpeg" "folder.jpeg" "Folder.jpeg" "FOLDER.jpeg"
|
|
"album.jpeg" "Album.jpeg" "ALBUM.jpeg" "front.jpeg" "Front.jpeg" "FRONT.jpeg"
|
|
"cover.png" "Cover.png" "COVER.png" "folder.png" "Folder.png" "FOLDER.png"
|
|
"album.png" "Album.png" "ALBUM.png" "front.png" "Front.png" "FRONT.png")
|
|
|
|
# Parse command line arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-o|--output)
|
|
OUTPUT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
-r|--replace)
|
|
REPLACE_MODE=true
|
|
shift
|
|
;;
|
|
-p|--preserve)
|
|
PRESERVE_MODE=true
|
|
shift
|
|
;;
|
|
-t|--test)
|
|
TEST_MODE=true
|
|
shift
|
|
;;
|
|
-f|--force)
|
|
FORCE_MODE=true
|
|
shift
|
|
;;
|
|
-b|--bitrate)
|
|
OPUS_BITRATE="$2"
|
|
shift 2
|
|
;;
|
|
--no-covers)
|
|
COPY_COVERS=false
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
-*)
|
|
echo "Unknown option: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
*)
|
|
MEDIA_LIBRARY="$1"
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Validate bitrate
|
|
if ! [[ "$OPUS_BITRATE" =~ ^[0-9]+$ ]] || [ "$OPUS_BITRATE" -lt 64 ] || [ "$OPUS_BITRATE" -gt 512 ]; then
|
|
echo "Error: Invalid bitrate '$OPUS_BITRATE'. Must be between 64 and 512 kbps"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if media library path is provided
|
|
if [ -z "$MEDIA_LIBRARY" ]; then
|
|
echo "Error: Media library path is required"
|
|
show_usage
|
|
exit 1
|
|
fi
|
|
|
|
# Check if media library exists
|
|
if [ ! -d "$MEDIA_LIBRARY" ]; then
|
|
echo "Error: Media library directory '$MEDIA_LIBRARY' not found"
|
|
exit 1
|
|
fi
|
|
|
|
# Set output directory if not specified
|
|
if [ -z "$OUTPUT_DIR" ] && [ "$REPLACE_MODE" = false ]; then
|
|
OUTPUT_DIR="${MEDIA_LIBRARY}_opus"
|
|
fi
|
|
|
|
# Function to check if file needs conversion
|
|
needs_conversion() {
|
|
local input_file="$1"
|
|
local output_file="$2"
|
|
|
|
# Check if input file is readable
|
|
if [ ! -r "$input_file" ]; then
|
|
echo " WARNING: File not readable: $input_file"
|
|
return 1
|
|
fi
|
|
|
|
# If force mode, always convert
|
|
if [ "$FORCE_MODE" = true ]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if output file already exists and is newer than input
|
|
if [ -f "$output_file" ] && [ "$output_file" -nt "$input_file" ]; then
|
|
return 1 # No conversion needed
|
|
fi
|
|
|
|
return 0 # Needs conversion
|
|
}
|
|
|
|
# Function to copy cover art files
|
|
copy_cover_art() {
|
|
local source_dir="$1"
|
|
local dest_dir="$2"
|
|
local relative_path="$3"
|
|
|
|
# Skip if covers are disabled
|
|
if [ "$COPY_COVERS" = false ]; then
|
|
return
|
|
fi
|
|
|
|
# Skip if in replace or preserve mode (covers stay in original location)
|
|
if [ "$REPLACE_MODE" = true ] || [ "$PRESERVE_MODE" = true ]; then
|
|
return
|
|
fi
|
|
|
|
# Look for cover art files
|
|
local found_cover=false
|
|
for cover_name in "${COVER_FILENAMES[@]}"; do
|
|
local cover_file="$source_dir/$cover_name"
|
|
if [ -f "$cover_file" ]; then
|
|
local dest_cover="$dest_dir/$cover_name"
|
|
|
|
# Check if we need to copy (force mode or destination doesn't exist/is older)
|
|
if [ "$FORCE_MODE" = true ] || [ ! -f "$dest_cover" ] || [ "$cover_file" -nt "$dest_cover" ]; then
|
|
if [ "$TEST_MODE" = true ]; then
|
|
echo " WOULD COPY COVER: $relative_path/$cover_name"
|
|
else
|
|
echo " COPYING COVER: $cover_name"
|
|
if cp "$cover_file" "$dest_cover" 2>/dev/null; then
|
|
((COVERS_COPIED++))
|
|
else
|
|
echo " WARNING: Failed to copy $cover_name"
|
|
fi
|
|
fi
|
|
found_cover=true
|
|
else
|
|
echo " SKIP COVER: $cover_name (already exists and is newer)"
|
|
((COVERS_SKIPPED++))
|
|
found_cover=true
|
|
fi
|
|
# Only copy the first cover file found to avoid duplicates
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ "$found_cover" = false ] && [ "$TEST_MODE" = false ]; then
|
|
echo " NO COVER: No cover art found in $(basename "$source_dir")"
|
|
fi
|
|
}
|
|
|
|
# Function to process a single FLAC file
|
|
process_flac_file() {
|
|
local input_file="$1"
|
|
local relative_path="$2"
|
|
local output_file=""
|
|
|
|
# Determine output file path
|
|
if [ "$REPLACE_MODE" = true ]; then
|
|
# Replace .flac with .ogg in same location
|
|
output_file="${input_file%.flac}.ogg"
|
|
elif [ "$PRESERVE_MODE" = true ]; then
|
|
local dir=$(dirname "$input_file")
|
|
local filename=$(basename "$input_file" .flac)
|
|
output_file="$dir/${filename}.ogg"
|
|
else
|
|
# Change extension from .flac to .ogg
|
|
local opus_relative_path="${relative_path%.flac}.ogg"
|
|
output_file="$OUTPUT_DIR/$opus_relative_path"
|
|
fi
|
|
|
|
# Check if conversion is needed
|
|
if ! needs_conversion "$input_file" "$output_file"; then
|
|
echo " SKIP: $relative_path (Opus file exists and is newer)"
|
|
((SKIPPED_COUNT++))
|
|
return
|
|
fi
|
|
|
|
if [ "$TEST_MODE" = true ]; then
|
|
echo " WOULD CONVERT: $relative_path → $(basename "$output_file") @ ${OPUS_BITRATE}kbps"
|
|
return
|
|
fi
|
|
|
|
# Create output directory if needed
|
|
local output_dir=$(dirname "$output_file")
|
|
if [ ! -d "$output_dir" ]; then
|
|
echo " Creating directory: $output_dir"
|
|
if ! mkdir -p "$output_dir"; then
|
|
echo " ✗ ERROR: Cannot create output directory: $output_dir"
|
|
((ERROR_COUNT++))
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# Verify input file exists and is readable
|
|
if [ ! -f "$input_file" ]; then
|
|
echo " ✗ ERROR: Input file not found: $input_file"
|
|
((ERROR_COUNT++))
|
|
return
|
|
fi
|
|
|
|
if [ ! -r "$input_file" ]; then
|
|
echo " ✗ ERROR: Input file not readable: $input_file"
|
|
((ERROR_COUNT++))
|
|
return
|
|
fi
|
|
|
|
# Create temporary file for safe processing
|
|
local temp_file="${output_file}.tmp.ogg"
|
|
|
|
echo " CONVERTING: $relative_path → Opus ${OPUS_BITRATE}kbps"
|
|
|
|
# Convert the file to Opus
|
|
echo " Command: ffmpeg -i \"$input_file\" -c:a libopus -b:a ${OPUS_BITRATE}k -vbr on -compression_level 10 -y \"$temp_file\""
|
|
|
|
if ffmpeg -i "$input_file" -c:a libopus -b:a "${OPUS_BITRATE}k" -vbr on -compression_level 10 -y "$temp_file" 2>"${temp_file}.log"; then
|
|
# Move temp file to final location
|
|
mv "$temp_file" "$output_file"
|
|
echo " ✓ DONE: $(basename "$output_file")"
|
|
rm -f "${temp_file}.log"
|
|
((PROCESSED_COUNT++))
|
|
|
|
# If replace mode, remove original FLAC file
|
|
if [ "$REPLACE_MODE" = true ]; then
|
|
rm "$input_file"
|
|
echo " Removed original: $(basename "$input_file")"
|
|
fi
|
|
else
|
|
echo " ✗ ERROR: Failed to convert $relative_path"
|
|
echo " Error log saved to: ${temp_file}.log"
|
|
if [ -f "${temp_file}.log" ]; then
|
|
echo " Last few lines of error:"
|
|
tail -n 5 "${temp_file}.log" | sed 's/^/ /'
|
|
fi
|
|
rm -f "$temp_file"
|
|
((ERROR_COUNT++))
|
|
fi
|
|
}
|
|
|
|
# Function to process an album directory
|
|
process_album() {
|
|
local album_dir="$1"
|
|
local album_name=$(basename "$album_dir")
|
|
local relative_album_path="${album_dir#$MEDIA_LIBRARY/}"
|
|
|
|
echo "Processing album: $album_name"
|
|
|
|
# Find all FLAC files in the album directory
|
|
local flac_files=()
|
|
while IFS= read -r -d '' file; do
|
|
flac_files+=("$file")
|
|
done < <(find "$album_dir" -name "*.flac" -type f -print0)
|
|
|
|
if [ ${#flac_files[@]} -eq 0 ]; then
|
|
echo " No FLAC files found in $album_name"
|
|
return
|
|
fi
|
|
|
|
# Process each FLAC file
|
|
for flac_file in "${flac_files[@]}"; do
|
|
local relative_path="${flac_file#$MEDIA_LIBRARY/}"
|
|
process_flac_file "$flac_file" "$relative_path"
|
|
done
|
|
|
|
# Copy cover art if needed (only for output directory mode)
|
|
if [ "$REPLACE_MODE" = false ] && [ "$PRESERVE_MODE" = false ]; then
|
|
local output_album_dir="$OUTPUT_DIR/$relative_album_path"
|
|
copy_cover_art "$album_dir" "$output_album_dir" "$relative_album_path"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
# Main processing
|
|
echo "Media Library FLAC to Opus Converter"
|
|
echo "===================================="
|
|
echo "Input: $MEDIA_LIBRARY"
|
|
echo "Bitrate: ${OPUS_BITRATE}kbps"
|
|
|
|
if [ "$TEST_MODE" = true ]; then
|
|
echo "Mode: TEST MODE (no files will be modified)"
|
|
elif [ "$REPLACE_MODE" = true ]; then
|
|
echo "Mode: REPLACE (original FLAC files will be deleted after conversion)"
|
|
elif [ "$PRESERVE_MODE" = true ]; then
|
|
echo "Mode: PRESERVE (Opus files created alongside originals)"
|
|
else
|
|
echo "Output: $OUTPUT_DIR"
|
|
fi
|
|
|
|
if [ "$FORCE_MODE" = true ]; then
|
|
echo "Force: Enabled (will re-convert existing Opus files)"
|
|
fi
|
|
|
|
if [ "$COPY_COVERS" = true ]; then
|
|
echo "Covers: Enabled (will copy album artwork)"
|
|
else
|
|
echo "Covers: Disabled"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Find all directories that contain FLAC files (potential albums)
|
|
album_dirs=()
|
|
while IFS= read -r -d '' dir; do
|
|
if find "$dir" -maxdepth 1 -name "*.flac" -type f | grep -q .; then
|
|
album_dirs+=("$dir")
|
|
fi
|
|
done < <(find "$MEDIA_LIBRARY" -type d -print0)
|
|
|
|
if [ ${#album_dirs[@]} -eq 0 ]; then
|
|
echo "No albums with FLAC files found in $MEDIA_LIBRARY"
|
|
exit 0
|
|
fi
|
|
|
|
echo "Found ${#album_dirs[@]} album(s) with FLAC files"
|
|
echo ""
|
|
|
|
# Process each album
|
|
for album_dir in "${album_dirs[@]}"; do
|
|
process_album "$album_dir"
|
|
done
|
|
|
|
# Summary
|
|
echo "Conversion Summary:"
|
|
echo "=================="
|
|
echo "Converted: $PROCESSED_COUNT files"
|
|
echo "Skipped: $SKIPPED_COUNT files (already converted or newer)"
|
|
echo "Errors: $ERROR_COUNT files"
|
|
|
|
if [ "$COPY_COVERS" = true ]; then
|
|
echo "Covers copied: $COVERS_COPIED files"
|
|
echo "Covers skipped: $COVERS_SKIPPED files (already exist or newer)"
|
|
fi
|
|
|
|
if [ "$ERROR_COUNT" -gt 0 ]; then
|
|
exit 1
|
|
fi
|