Files
shellscripts/flac-opus-compressing.sh
2025-08-22 08:20:44 +07:00

594 lines
19 KiB
Bash
Executable File

#!/bin/bash
# Media Library FLAC to Opus Conversion Script
# Uses opus-tools (opusenc) for high-quality FLAC to Opus conversion
# Also copies cover art files to maintain album artwork
# Check if required tools are installed
check_dependencies() {
local missing_tools=()
if ! command -v opusenc &> /dev/null; then
missing_tools+=("opusenc (from opus-tools package)")
fi
if ! command -v flac &> /dev/null; then
missing_tools+=("flac (for metadata extraction)")
fi
if [ ${#missing_tools[@]} -gt 0 ]; then
echo "Error: Missing required tools:"
for tool in "${missing_tools[@]}"; do
echo " - $tool"
done
echo ""
echo "To install missing dependencies:"
echo " Ubuntu/Debian: sudo apt install opus-tools flac"
echo " macOS: brew install opus-tools flac"
echo " Arch Linux: sudo pacman -S opus-tools flac"
echo " Fedora: sudo dnf install opus-tools flac"
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: 192)"
echo " -q, --quality LEVEL Use VBR quality instead of bitrate (0-10, 10=best)"
echo " --no-covers Skip copying cover art files"
echo " --no-metadata Skip copying metadata from FLAC 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 256 /path/to/music/library"
echo " $0 -q 8 /path/to/music/library # VBR quality 8 (very good) with .opus.ogg extension"
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
COPY_METADATA=true
OPUS_BITRATE=192
OPUS_QUALITY=""
USE_VBR_QUALITY=false
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"
USE_VBR_QUALITY=false
shift 2
;;
-q|--quality)
OPUS_QUALITY="$2"
USE_VBR_QUALITY=true
shift 2
;;
--no-covers)
COPY_COVERS=false
shift
;;
--no-metadata)
COPY_METADATA=false
shift
;;
-h|--help)
show_usage
exit 0
;;
-*)
echo "Unknown option: $1"
show_usage
exit 1
;;
*)
MEDIA_LIBRARY="$1"
shift
;;
esac
done
# Check dependencies
check_dependencies
# Validate quality or bitrate settings
if [ "$USE_VBR_QUALITY" = true ]; then
if ! [[ "$OPUS_QUALITY" =~ ^[0-9]+$ ]] || [ "$OPUS_QUALITY" -lt 0 ] || [ "$OPUS_QUALITY" -gt 10 ]; then
echo "Error: Invalid quality level '$OPUS_QUALITY'. Must be between 0 and 10"
echo "Quality levels: 0 (lowest), 5 (good), 8 (very good), 10 (best)"
exit 1
fi
else
if ! [[ "$OPUS_BITRATE" =~ ^[0-9]+$ ]] || [ "$OPUS_BITRATE" -lt 6 ] || [ "$OPUS_BITRATE" -gt 510 ]; then
echo "Error: Invalid bitrate '$OPUS_BITRATE'. Must be between 6 and 510 kbps"
echo "Recommended values: 96 (good), 128 (very good), 192 (excellent), 256 (overkill)"
exit 1
fi
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 extract embedded cover art from FLAC file
extract_flac_cover_art() {
local flac_file="$1"
local temp_cover_file="$2"
if [ "$COPY_COVERS" = false ]; then
return 1
fi
# Try to extract embedded cover art using metaflac
if command -v metaflac &> /dev/null; then
# Check if the FLAC file has embedded pictures
if metaflac --list --block-type=PICTURE "$flac_file" 2>/dev/null | grep -q "PICTURE"; then
# Extract the first picture (cover art)
if metaflac --export-picture-to="$temp_cover_file" "$flac_file" 2>/dev/null; then
return 0
fi
fi
fi
# Fallback: try ffmpeg to extract embedded art
if command -v ffmpeg &> /dev/null; then
if ffmpeg -hide_banner -y -i "$flac_file" -an -vcodec copy "$temp_cover_file" 2>/dev/null; then
# Check if the extracted file has content
if [ -s "$temp_cover_file" ]; then
return 0
fi
fi
fi
return 1
}
# Function to extract metadata from FLAC file
extract_flac_metadata() {
local flac_file="$1"
local temp_tags_file="$2"
if [ "$COPY_METADATA" = false ]; then
return
fi
# Extract metadata using metaflac (preferred method)
if command -v metaflac &> /dev/null; then
metaflac --export-tags-to="$temp_tags_file" "$flac_file" 2>/dev/null
# If metaflac succeeded and file has content, we're done
if [ -s "$temp_tags_file" ]; then
return
fi
fi
# Fallback: use ffprobe if available (often more comprehensive)
if command -v ffprobe &> /dev/null; then
ffprobe -v quiet -show_entries format_tags -of csv=p=0:s=x "$flac_file" 2>/dev/null | \
sed 's/^tag://; s/x/\n/g' | grep '=' > "$temp_tags_file"
# If ffprobe succeeded and file has content, we're done
if [ -s "$temp_tags_file" ]; then
return
fi
fi
# Last fallback: use flac command to list tags
if command -v flac &> /dev/null; then
flac --list --block-type=VORBIS_COMMENT "$flac_file" 2>/dev/null | \
grep -E "^\s+comment\[[0-9]+\]:" | \
sed 's/^\s*comment\[[0-9]*\]: //' > "$temp_tags_file"
fi
}
# 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 with .opus.ogg extension
if [ "$REPLACE_MODE" = true ]; then
# Replace .flac with .opus.ogg in same location
output_file="${input_file%.flac}.opus.ogg"
elif [ "$PRESERVE_MODE" = true ]; then
local dir=$(dirname "$input_file")
local filename=$(basename "$input_file" .flac)
output_file="$dir/${filename}.opus.ogg"
else
# Change extension from .flac to .opus.ogg
local opus_relative_path="${relative_path%.flac}.opus.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
if [ "$USE_VBR_QUALITY" = true ]; then
echo " WOULD CONVERT: $relative_path$(basename "$output_file") @ VBR quality ${OPUS_QUALITY}"
else
echo " WOULD CONVERT: $relative_path$(basename "$output_file") @ ${OPUS_BITRATE}kbps"
fi
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 files
local temp_file="${output_file}.tmp"
local temp_cover_file="${output_file}.cover.tmp"
# Extract embedded cover art from FLAC file
local has_embedded_cover=false
if extract_flac_cover_art "$input_file" "$temp_cover_file"; then
has_embedded_cover=true
echo " Found embedded cover art"
fi
# Look for external cover art if no embedded cover found
local external_cover_file=""
if [ "$has_embedded_cover" = false ]; then
local source_dir=$(dirname "$input_file")
for cover_name in "${COVER_FILENAMES[@]}"; do
local cover_file="$source_dir/$cover_name"
if [ -f "$cover_file" ]; then
external_cover_file="$cover_file"
echo " Found external cover art: $cover_name"
break
fi
done
fi
# Build opusenc command
local opusenc_cmd=(opusenc)
# Add quality or bitrate settings
if [ "$USE_VBR_QUALITY" = true ]; then
opusenc_cmd+=(--vbr --comp "$OPUS_QUALITY")
echo " CONVERTING: $relative_path → Opus VBR quality ${OPUS_QUALITY}"
else
opusenc_cmd+=(--bitrate "$OPUS_BITRATE")
echo " CONVERTING: $relative_path → Opus ${OPUS_BITRATE}kbps"
fi
# Add embedded cover art if available
if [ "$has_embedded_cover" = true ] && [ -f "$temp_cover_file" ]; then
opusenc_cmd+=(--picture "$temp_cover_file")
echo " Embedding cover art from FLAC"
elif [ -n "$external_cover_file" ]; then
opusenc_cmd+=(--picture "$external_cover_file")
echo " Embedding external cover art"
fi
# Note: opusenc automatically copies metadata from FLAC files
# Only add manual metadata if we need to override or add specific tags
if [ "$COPY_METADATA" = true ]; then
echo " Metadata: opusenc will automatically copy FLAC tags"
else
# If metadata copying is disabled, use --discard-comments
opusenc_cmd+=(--discard-comments)
echo " Metadata: Disabled (using --discard-comments)"
fi
# Add input and output files
opusenc_cmd+=("$input_file" "$temp_file")
echo " Command: ${opusenc_cmd[*]}"
# Run opusenc with error logging
if "${opusenc_cmd[@]}" 2>"${temp_file}.log"; then
# Verify the output file was created and has content
if [ -s "$temp_file" ]; then
# Move temp file to final location
mv "$temp_file" "$output_file"
echo " ✓ DONE: $(basename "$output_file")"
rm -f "${temp_file}.log" "$temp_cover_file"
((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: Output file is empty or wasn't created properly"
rm -f "$temp_file" "$temp_cover_file"
((ERROR_COUNT++))
fi
else
echo " ✗ ERROR: Failed to convert $relative_path"
echo " Error log saved to: ${temp_file}.log"
if [ -f "${temp_file}.log" ]; then
echo " Error details:"
tail -n 5 "${temp_file}.log" | sed 's/^/ /'
fi
rm -f "$temp_file" "$temp_cover_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 ""
}
# Test opusenc with version info
echo "Testing opus-tools..."
opusenc_version=$(opusenc --version 2>&1 | head -n1)
echo "Found: $opusenc_version"
# Main processing
echo ""
echo "Media Library FLAC to Opus Converter (using opus-tools)"
echo "======================================================="
echo "Input: $MEDIA_LIBRARY"
if [ "$USE_VBR_QUALITY" = true ]; then
echo "Quality: VBR level ${OPUS_QUALITY} (0=lowest, 10=best)"
else
echo "Bitrate: ${OPUS_BITRATE}kbps CBR"
fi
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 embed cover art in Opus files and copy external files)"
else
echo "Covers: Disabled"
fi
if [ "$COPY_METADATA" = true ]; then
echo "Metadata: Enabled (will copy tags from FLAC files)"
else
echo "Metadata: 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
echo ""
echo "Some files failed to convert. Check the error logs for details."
exit 1
fi