diff --git a/flac-opus-compressing.sh b/flac-opus-compressing.sh index 6f96aef..218dbbc 100755 --- a/flac-opus-compressing.sh +++ b/flac-opus-compressing.sh @@ -1,21 +1,35 @@ #!/bin/bash # Media Library FLAC to Opus Conversion Script -# Processes multiple albums in a media library, converting all FLAC files to Opus 448kbps +# Uses opus-tools (opusenc) for high-quality FLAC to Opus conversion # 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 +# 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() { @@ -27,8 +41,10 @@ show_usage() { 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 " -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:" @@ -36,7 +52,8 @@ show_usage() { 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 -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" } @@ -47,7 +64,10 @@ 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 @@ -87,12 +107,22 @@ while [[ $# -gt 0 ]]; do ;; -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 @@ -109,10 +139,22 @@ while [[ $# -gt 0 ]]; do 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 +# 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 @@ -157,6 +199,77 @@ needs_conversion() { 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" @@ -214,17 +327,17 @@ process_flac_file() { local relative_path="$2" local output_file="" - # Determine output file path + # Determine output file path with .opus.ogg extension if [ "$REPLACE_MODE" = true ]; then - # Replace .flac with .ogg in same location - output_file="${input_file%.flac}.ogg" + # 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}.ogg" + output_file="$dir/${filename}.opus.ogg" else - # Change extension from .flac to .ogg - local opus_relative_path="${relative_path%.flac}.ogg" + # Change extension from .flac to .opus.ogg + local opus_relative_path="${relative_path%.flac}.opus.ogg" output_file="$OUTPUT_DIR/$opus_relative_path" fi @@ -236,7 +349,11 @@ process_flac_file() { 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 @@ -264,34 +381,95 @@ process_flac_file() { return fi - # Create temporary file for safe processing - local temp_file="${output_file}.tmp.ogg" + # Create temporary files + local temp_file="${output_file}.tmp" + local temp_cover_file="${output_file}.cover.tmp" - echo " CONVERTING: $relative_path → Opus ${OPUS_BITRATE}kbps" + # 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 - # 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\"" + # 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 - 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")" + # 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 " Last few lines of error:" - tail -n 5 "${temp_file}.log" | sed 's/^/ /' + echo " Error details:" + tail -n 5 "${temp_file}.log" | sed 's/^/ /' fi - rm -f "$temp_file" + rm -f "$temp_file" "$temp_cover_file" ((ERROR_COUNT++)) fi } @@ -330,11 +508,22 @@ process_album() { 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 "Media Library FLAC to Opus Converter" -echo "====================================" +echo "" +echo "Media Library FLAC to Opus Converter (using opus-tools)" +echo "=======================================================" echo "Input: $MEDIA_LIBRARY" -echo "Bitrate: ${OPUS_BITRATE}kbps" + +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)" @@ -351,11 +540,17 @@ if [ "$FORCE_MODE" = true ]; then fi if [ "$COPY_COVERS" = true ]; then - echo "Covers: Enabled (will copy album artwork)" + 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) @@ -392,5 +587,7 @@ if [ "$COPY_COVERS" = true ]; then fi if [ "$ERROR_COUNT" -gt 0 ]; then + echo "" + echo "Some files failed to convert. Check the error logs for details." exit 1 fi