#!/bin/bash # gempub-converter.sh # Conversor EPUB to Gempub # Uso: gempub-converter.sh -e archivo.epub | -c carpeta set -e # Colores para output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Funciones de log log_info() { echo -e "${BLUE}[INFO]${NC} $1" >&2; } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } # Mostrar ayuda show_help() { cat << EOF Uso: $0 [OPCIÓN]... [ARCHIVO|CARPETA] Conversor EPUB to Gempub Opciones: -e, --extraer ARCHIVO.epub Extrae y convierte un EPUB a estructura Gempub -c, --crear CARPETA Crea archivo .gpub desde carpeta Gempub -h, --help Muestra esta ayuda Ejemplos: $0 -e libro.epub $0 -c mi_libro_gempub/ EOF } # Función para verificar si un capítulo está completamente en blanco es_capitulo_en_blanco() { local archivo="$1" if [ ! -f "$archivo" ] || [ ! -s "$archivo" ]; then return 0 # Está completamente vacío fi # Contar caracteres no espaciales local caracteres_utiles=$(cat "$archivo" | tr -d '[:space:]' | wc -c) # Considerar en blanco si tiene menos de 2 caracteres útiles if [ "$caracteres_utiles" -lt 2 ]; then return 0 # Está en blanco else return 1 # Tiene contenido fi } # Función para comprimir imagen si es grande comprimir_imagen() { local img_file="$1" local dest_dir="$2" # Verificar si la imagen existe if [ ! -f "$img_file" ]; then log_warning "Imagen no encontrada: $img_file" return 1 fi # Obtener información de la imagen local file_size=$(stat -f%z "$img_file" 2>/dev/null || stat -c%s "$img_file" 2>/dev/null) local file_name=$(basename "$img_file") local file_ext=$(echo "$file_name" | rev | cut -d. -f1 | rev | tr '[:upper:]' '[:lower:]') local file_base=$(basename "$file_name" ".$file_ext") # Si la imagen es menor a 1MB, copiar sin comprimir if [ "$file_size" -lt 1048576 ]; then cp "$img_file" "$dest_dir/$file_name" echo "$file_name" # SOLO el nombre del archivo por stdout return 0 fi # Verificar si tenemos herramientas de conversión if ! command -v convert &> /dev/null && ! command -v ffmpeg &> /dev/null; then log_warning "No hay herramientas de conversión (ImageMagick/ffmpeg), copiando sin comprimir" cp "$img_file" "$dest_dir/$file_name" echo "$file_name" # SOLO el nombre del archivo por stdout return 0 fi # Para imágenes grandes (>1MB), convertir a JPEG local jpg_name="${file_base}.jpg" local jpg_path="$dest_dir/$jpg_name" log_info "Comprimiendo imagen grande: $file_name ($((file_size/1024/1024))MB) → $jpg_name" # Intentar con ImageMagick (convert) primero local conversion_exitcode=1 if command -v convert &> /dev/null; then if convert "$img_file" -quality 85 -strip "$jpg_path" 2>/dev/null; then conversion_exitcode=0 fi fi # Fallback a ffmpeg si ImageMagick falló if [ $conversion_exitcode -ne 0 ] && command -v ffmpeg &> /dev/null; then if ffmpeg -i "$img_file" -q:v 5 -y "$jpg_path" 2>/dev/null; then conversion_exitcode=0 fi fi # Verificar si la conversión fue exitosa if [ $conversion_exitcode -eq 0 ] && [ -f "$jpg_path" ]; then local new_size=$(stat -f%z "$jpg_path" 2>/dev/null || stat -c%s "$jpg_path" 2>/dev/null) local reduction=$((100 - (new_size * 100 / file_size))) log_success "Comprimida: $file_name → $jpg_name (${reduction}% reducción)" echo "$jpg_name" # SOLO el nombre del archivo por stdout return 0 else log_warning "Falló la conversión, copiando original" cp "$img_file" "$dest_dir/$file_name" echo "$file_name" # SOLO el nombre del archivo por stdout return 1 fi } # Función para actualizar referencias a imágenes en archivos .gmi actualizar_referencias_imagenes() { local work_dir="$1" local old_name="$2" local new_name="$3" # Actualizar en todos los archivos .gmi find "$work_dir" -name "*.gmi" -type f | while read -r gmi_file; do if grep -q "$old_name" "$gmi_file"; then sed -i.tmp "s|$old_name|$new_name|g" "$gmi_file" rm -f "$gmi_file.tmp" log_info "Actualizada referencia en $(basename "$gmi_file"): $old_name → $new_name" fi done # Actualizar en metadata.txt si es la portada if grep -q "cover:.*$old_name" "$work_dir/metadata.txt"; then sed -i.tmp "s|cover:.*$old_name|cover: imagenes/$new_name|g" "$work_dir/metadata.txt" rm -f "$work_dir/metadata.txt.tmp" log_info "Actualizada portada en metadata.txt: $old_name → $new_name" fi } # Función para limpiar texto para Gemtext clean_text_for_gemtext() { local text="$1" # Eliminar caracteres problemáticos pero mantener acentos y caracteres especiales válidos echo "$text" | sed -e 's/[“]/\"/g' -e 's/[”]/\"/g' -e 's/[‘]/'"'"'/g' -e 's/[’]/'"'"'/g' -e 's/…/.../g' -e 's/–/-/g' -e 's/—/--/g' } # Función para obtener primera línea de un archivo (limpia) get_first_line_clean() { local file="$1" if [ ! -f "$file" ] || [ ! -s "$file" ]; then echo "Capítulo" return fi # Obtener primera línea no vacía y limpiarla local first_line=$(grep -v '^[[:space:]]*$' "$file" | head -1 | sed 's/^[#]*[[:space:]]*//' | head -c 200) if [ -z "$first_line" ]; then echo "Capítulo" else echo "$first_line" fi } # Función para verificar si es capítulo "subtítulo" is_subtitle_chapter() { local file="$1" if [ ! -f "$file" ] || [ ! -s "$file" ]; then return 1 fi # Contar líneas no vacías local non_empty_lines=$(grep -v '^[[:space:]]*$' "$file" | wc -l) local content_length=$(cat "$file" | wc -c) # Es subtítulo si: 1 línea no vacía Y menos de 200 caracteres if [ "$non_empty_lines" -eq 1 ] && [ "$content_length" -lt 200 ]; then return 0 else return 1 fi } # Función para extraer texto alternativo de imagen HTML extraer_alt_text() { local html_line="$1" # Extraer texto alternativo de la etiqueta img local alt_text=$(echo "$html_line" | sed -n 's/.*alt="\([^"]*\)".*/\1/pI') if [ -z "$alt_text" ]; then # Intentar con comillas simples alt_text=$(echo "$html_line" | sed -n "s/.*alt='\([^']*\)'.*/\1/pI") fi if [ -z "$alt_text" ]; then # Usar nombre del archivo como fallback (mejorado) local img_src=$(extraer_img_src "$html_line") if [ -n "$img_src" ]; then alt_text=$(basename "$img_src" | sed 's/\.[^.]*$//' | tr '_-' ' ' | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g') # Capitalizar primera letra alt_text=$(echo "$alt_text" | sed 's/^./\U&/') else alt_text="Imagen" fi fi # Limpiar el texto alternativo alt_text=$(echo "$alt_text" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') echo "$alt_text" } # Función para extraer src de imagen HTML extraer_img_src() { local html_line="$1" # Extraer src de la etiqueta img local img_src=$(echo "$html_line" | sed -n 's/.*src="\([^"]*\)".*/\1/pI') if [ -z "$img_src" ]; then # Intentar con comillas simples img_src=$(echo "$html_line" | sed -n "s/.*src='\([^']*\)'.*/\1/pI") fi echo "$img_src" } # Función para convertir HTML/XHTML a Gemtext (VERSIÓN MEJORADA Y ROBUSTA) html_to_gemtext() { local html_file="$1" local gemtext_file="$2" log_info "Procesando: $(basename "$html_file")" # Verificar que el archivo existe y no está vacío if [ ! -f "$html_file" ] || [ ! -s "$html_file" ]; then log_warning "Archivo no encontrado o vacío: $html_file" echo "# Archivo no disponible" > "$gemtext_file" return 1 fi # Archivo temporal para el procesamiento local temp_file="$gemtext_file.tmp" # PRIMERO: Extraer y procesar todas las imágenes del HTML local image_count=0 declare -a image_lines declare -a image_replacements # Leer el HTML y encontrar todas las líneas con imágenes while IFS= read -r line; do if echo "$line" | grep -qi ' /dev/null; then if pandoc "$html_file" -f html -t plain --wrap=none 2>/dev/null > "$temp_file"; then log_info "Usando pandoc para conversión HTML→texto" else # Fallback a conversión básica si pandoc falla log_warning "Pandoc falló, usando conversión básica" cat "$html_file" | sed -e 's/<[^>]*>//g' -e 's/ / /g' -e 's/&/\&/g' > "$temp_file" fi else # Conversión básica con sed cat "$html_file" | sed -e 's/<[^>]*>//g' -e 's/ / /g' -e 's/&/\&/g' > "$temp_file" fi # TERCERO: Aplicar limpieza al texto clean_text_for_gemtext "$(cat "$temp_file")" > "$temp_file.clean" mv "$temp_file.clean" "$temp_file" # CUARTO: Si hay imágenes, crear un archivo final combinando texto e imágenes if [ $image_count -gt 0 ]; then local final_content="" local line_num=1 # Leer el HTML original línea por línea para mantener el orden correcto while IFS= read -r original_line; do local image_found=0 # Verificar si esta línea es una imagen que debemos reemplazar for ((i=0; i/dev/null || true) if [ -n "$text_line" ] && [ ! "$text_line" = "..." ]; then final_content="${final_content}${text_line}"$'\n' fi line_num=$((line_num + 1)) fi done < "$html_file" # Escribir el contenido final echo "$final_content" > "$gemtext_file" else # Si no hay imágenes, usar directamente el texto convertido mv "$temp_file" "$gemtext_file" fi # Limpiar archivos temporales rm -f "$temp_file" "$temp_file.clean" # QUINTO: Post-procesamiento para asegurar formato Gemtext válido { # Asegurar que el archivo termina con nueva línea cat "$gemtext_file" echo } > "$gemtext_file.tmp" mv "$gemtext_file.tmp" "$gemtext_file" # Eliminar líneas completamente vacías consecutivas (máximo 2 líneas vacías seguidas) sed -i.tmp '/^[[:space:]]*$/{N;/^\n$/d;}' "$gemtext_file" rm -f "$gemtext_file.tmp" # Si el archivo resultante está vacío, poner contenido mínimo if [ ! -s "$gemtext_file" ]; then echo "# Capítulo" > "$gemtext_file" echo "" >> "$gemtext_file" echo "Contenido no disponible en este capítulo." >> "$gemtext_file" fi # Contar imágenes procesadas en el archivo final local final_image_count=$(grep -c '=> imagenes/' "$gemtext_file" || true) if [ $final_image_count -gt 0 ]; then log_success "Convertido con $final_image_count imagen(es): $(basename "$gemtext_file")" else log_success "Convertido: $(basename "$gemtext_file")" fi } # Función para extraer metadatos de OPF extract_metadata_from_opf() { local opf_file="$1" local metadata_file="$2" log_info "Extrayendo metadatos de: $(basename "$opf_file")" # Valores por defecto local title="" local author="" local language="" local description="" local published="" local publishDate="" local revisionDate="" local copyright="" local license="" local publisher="" local identifier="" local subject="" local cover_image="" # Extraer metadatos del OPF usando métodos más robustos if [ -f "$opf_file" ]; then local opf_content=$(cat "$opf_file") # EXTRAER TÍTULO - múltiples métodos title=$(echo "$opf_content" | grep -o '[^<]*' | sed 's///' | head -1) if [ -z "$title" ]; then title=$(echo "$opf_content" | grep -o '[^<]*' | sed 's/<title>//' | head -1) fi # EXTRAER AUTOR - métodos más agresivos author=$(echo "$opf_content" | grep -o '<dc:creator[^>]*>[^<]*' | sed 's/<dc:creator[^>]*>//' | head -1) if [ -z "$author" ]; then author=$(echo "$opf_content" | grep -o '<creator[^>]*>[^<]*' | sed 's/<creator[^>]*>//' | head -1) fi if [ -z "$author" ]; then # Buscar en cualquier tag que contenga "creator" author=$(echo "$opf_content" | grep -i 'creator' | grep -o '>[^<]*<' | sed 's/^>//;s/<$//' | head -1) fi # EXTRAER DESCRIPCIÓN description=$(echo "$opf_content" | grep -o '<dc:description>[^<]*' | sed 's/<dc:description>//' | head -1) # EXTRAER IDIOMA language=$(echo "$opf_content" | grep -o '<dc:language>[^<]*' | sed 's/<dc:language>//' | head -1) # EXTRAER FECHA - manejar diferentes formatos local date_line=$(echo "$opf_content" | grep -o '<dc:date>[^<]*' | head -1 | sed 's/<dc:date>//') if [ -n "$date_line" ]; then # Extraer YYYY-MM-DD si existe if [[ "$date_line" =~ ([0-9]{4}-[0-9]{2}-[0-9]{2}) ]]; then publishDate="${BASH_REMATCH[1]}" published=$(echo "$publishDate" | cut -d'-' -f1) # O solo el año elif [[ "$date_line" =~ ([0-9]{4}) ]]; then published="${BASH_REMATCH[1]}" fi fi # EXTRAER SUBJECT/TEMA subject=$(echo "$opf_content" | grep -o '<dc:subject>[^<]*' | sed 's/<dc:subject>//' | head -1) # EXTRAER IDENTIFICADOR identifier=$(echo "$opf_content" | grep -o '<dc:identifier[^>]*>[^<]*' | sed 's/<dc:identifier[^>]*>//' | head -1) # BUSCAR PORTADA en meta tags cover_image=$(echo "$opf_content" | grep -i 'meta.*name="cover"' | grep -o 'content="[^"]*"' | sed 's/content="//;s/"//') # EXTRAER COPYRIGHT si existe copyright=$(echo "$opf_content" | grep -o '<dc:rights>[^<]*' | sed 's/<dc:rights>//' | head -1) # EXTRAER EDITOR publisher=$(echo "$opf_content" | grep -o '<dc:publisher>[^<]*' | sed 's/<dc:publisher>//' | head -1) # DEBUG: Mostrar lo que se encontró log_info "Encontrado - Título: '$title', Autor: '$author', Descripción: '$description'" fi # Limpiar valores (eliminar espacios y caracteres extraños) title=$(echo "$title" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/&/&/g; s/</</g; s/>/>/g; s/"/"/g' | head -c 200) author=$(echo "$author" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/&/&/g; s/</</g; s/>/>/g; s/"/"/g' | head -c 150) description=$(echo "$description" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/&/&/g; s/</</g; s/>/>/g; s/"/"/g' | head -c 300) subject=$(echo "$subject" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | head -c 100) copyright=$(echo "$copyright" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | head -c 100) # Crear archivo de metadatos Gempub { echo "title: ${title:-Sin título}" echo "gpubVersion: 1.0.0" # Solo añadir campos que tienen contenido if [ -n "$author" ] && [ "$author" != "Sin título" ]; then echo "author: $author" fi if [ -n "$language" ] && [ "$language" != "Sin título" ]; then echo "language: $language" fi if [ -n "$description" ] && [ "$description" != "Sin título" ]; then echo "description: $description" fi if [ -n "$published" ]; then echo "published: $published" fi if [ -n "$publishDate" ]; then echo "publishDate: $publishDate" fi if [ -n "$subject" ] && [ "$subject" != "Sin título" ]; then echo "subject: $subject" fi if [ -n "$identifier" ] && [ "$identifier" != "Sin título" ]; then echo "identifier: $identifier" fi if [ -n "$copyright" ] && [ "$copyright" != "Sin título" ]; then echo "copyright: $copyright" fi if [ -n "$publisher" ] && [ "$publisher" != "Sin título" ]; then echo "publisher: $publisher" fi # Añadir créditos de conversión echo "version: Convertido a Gempub usando gempub-converter.sh" # Añadir charset por defecto echo "charset: UTF-8" } > "$metadata_file" # Verificar que se escribieron los campos críticos if grep -q "author:" "$metadata_file"; then log_success "Metadatos extraídos: '$title' por '$author'" else log_success "Metadatos extraídos: '$title' (autor no encontrado)" fi } # Función para encontrar archivo OPF find_opf_file() { local epub_dir="$1" # Buscar en META-INF/container.xml primero local container_file="$epub_dir/META-INF/container.xml" if [ -f "$container_file" ]; then log_info "Buscando OPF en container.xml" # Método más robusto para extraer la ruta local opf_path=$(grep -o 'full-path="[^"]*"' "$container_file" | head -1 | sed 's/full-path="//' | sed 's/"//') if [ -n "$opf_path" ] && [ -f "$epub_dir/$opf_path" ]; then echo "$epub_dir/$opf_path" return 0 fi fi # Buscar cualquier archivo .opf en el EPUB log_info "Buscando archivos OPF..." local opf_files=$(find "$epub_dir" -name "*.opf" -type f) if [ -n "$opf_files" ]; then local first_opf=$(echo "$opf_files" | head -1) log_info "Usando OPF encontrado: $(basename "$first_opf")" echo "$first_opf" return 0 fi log_warning "No se encontró archivo OPF" return 1 } # Función para encontrar archivos de contenido find_content_files() { local epub_dir="$1" local opf_file="$2" log_info "Buscando archivos de contenido..." # Si tenemos OPF, buscar en el manifest if [ -f "$opf_file" ] && command -v xmllint &> /dev/null; then local content_dir=$(dirname "$opf_file") # Extraer items del manifest local manifest_items=$(xmllint --xpath '//*[local-name()="item"]/@href' "$opf_file" 2>/dev/null | \ sed 's/href="/\n/g' | sed 's/"/\n/g' | grep -v '^$' | grep -E '\.(x?html|htm)$') if [ -n "$manifest_items" ]; then local files="" for item in $manifest_items; do local full_path="$content_dir/$item" if [ -f "$full_path" ]; then files="$files$full_path"$'\n' fi done echo "$files" | grep -v '^$' return 0 fi fi # Fallback: buscar todos los archivos HTML en el EPUB log_info "Buscando archivos HTML en todo el EPUB..." find "$epub_dir" -type f \( -name "*.html" -o -name "*.xhtml" -o -name "*.htm" \) | \ grep -v -E '(nav|toc|cover|titlepage|copyright|frontmatter|backmatter)' | \ sort } # Función para extraer EPUB (CON LÓGICA CORRECTA DE CAPÍTULOS) extract_epub() { local epub_file="$1" if [ ! -f "$epub_file" ]; then log_error "Archivo EPUB no encontrado: $epub_file" exit 1 fi local base_name=$(basename "$epub_file" .epub) local base_name=$(basename "$base_name" .EPUB) # Limpiar nombre para directorio local clean_name=$(echo "$base_name" | sed 's/[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑ -]//g') local work_dir="${clean_name}_gempub" log_info "Creando directorio de trabajo: $work_dir" mkdir -p "$work_dir" log_info "Extrayendo EPUB: $(basename "$epub_file")" if ! unzip -q "$epub_file" -d "$work_dir/epub_content" 2>/dev/null; then log_error "Error al extraer el EPUB" exit 1 fi # Encontrar y procesar metadatos local opf_file=$(find_opf_file "$work_dir/epub_content") if [ -n "$opf_file" ] && [ -f "$opf_file" ]; then extract_metadata_from_opf "$opf_file" "$work_dir/metadata.txt" else log_warning "No se encontró archivo OPF, usando metadatos por defecto" cat > "$work_dir/metadata.txt" << EOF title: $clean_name gpubVersion: 1.0.0 version: Convertido a Gempub usando gempub-converter.sh charset: UTF-8 EOF fi # Buscar archivos de contenido local html_files=$(find_content_files "$work_dir/epub_content" "$opf_file") if [ -z "$html_files" ]; then log_error "No se encontraron archivos de contenido HTML" exit 1 fi # Convertir contenido a Gemtext local chapter_count=0 local book_title=$(grep "^title:" "$work_dir/metadata.txt" | cut -d: -f2- | sed 's/^[[:space:]]*//') local index_content="# ${book_title:-Índice del Libro}\n\n" # Primero convertir todos los archivos declare -a gemtext_files while IFS= read -r html_file; do if [ ! -f "$html_file" ] || [ ! -s "$html_file" ]; then log_warning "Archivo no válido, saltando: $html_file" continue fi chapter_count=$((chapter_count + 1)) local gemtext_file="$work_dir/capitulo${chapter_count}.gmi" html_to_gemtext "$html_file" "$gemtext_file" gemtext_files[$chapter_count]="$gemtext_file" done <<< "$html_files" # PROCESAR CAPÍTULOS CON LÓGICA CORRECTA log_info "Procesando estructura de capítulos..." local capitulos_eliminados=0 local capitulos_subtitulo=0 local nuevo_numero=0 declare -a gemtext_files_finales for ((i=1; i<=chapter_count; i++)); do local gemtext_file="${gemtext_files[$i]}" # 1. ELIMINAR completamente vacíos (no aparecen en índice) if es_capitulo_en_blanco "$gemtext_file"; then log_warning "Eliminando capítulo completamente en blanco: capitulo${i}.gmi" rm -f "$gemtext_file" capitulos_eliminados=$((capitulos_eliminados + 1)) continue fi # 2. DETECTAR subtítulos (1 línea, <200 chars) → ## en índice + ELIMINAR archivo if is_subtitle_chapter "$gemtext_file"; then local contenido=$(get_first_line_clean "$gemtext_file") index_content="${index_content}## $contenido\n\n" log_info "Identificado como subtítulo: $contenido" rm -f "$gemtext_file" # ELIMINAR el archivo capitulos_subtitulo=$((capitulos_subtitulo + 1)) continue fi # 3. CAPÍTULOS NORMALES → => en índice con primera línea nuevo_numero=$((nuevo_numero + 1)) local nuevo_nombre="$work_dir/capitulo${nuevo_numero}.gmi" # Renombrar si es necesario (para mantener numeración secuencial) if [ "$gemtext_file" != "$nuevo_nombre" ]; then mv "$gemtext_file" "$nuevo_nombre" fi gemtext_files_finales[$nuevo_numero]="$nuevo_nombre" local first_line=$(get_first_line_clean "$nuevo_nombre") index_content="${index_content}=> capitulo${nuevo_numero}.gmi $first_line\n" done local total_capitulos_finales=$nuevo_numero # Crear archivo índice echo -e "$index_content" > "$work_dir/index.gmi" # Buscar y procesar imágenes log_info "Buscando y procesando imágenes..." local image_files=$(find "$work_dir/epub_content" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" -o -name "*.bmp" -o -name "*.gif" \) 2>/dev/null | head -20) if [ -n "$image_files" ]; then mkdir -p "$work_dir/imagenes" local image_count=0 while IFS= read -r img_file; do image_count=$((image_count + 1)) # Capturar SOLO el nombre del archivo, sin logs local final_name final_name=$(comprimir_imagen "$img_file" "$work_dir/imagenes") # Limpiar cualquier código de color que pueda haber escapado final_name=$(echo "$final_name" | sed 's/\x1b\[[0-9;]*m//g' | tr -d '\n') # Actualizar referencias en archivos .gmi si el nombre cambió local original_name=$(basename "$img_file") if [ "$final_name" != "$original_name" ]; then actualizar_referencias_imagenes "$work_dir" "$original_name" "$final_name" fi # Si es la primera imagen, establecer como portada if [ $image_count -eq 1 ]; then # Asegurarse de que el campo cover esté limpio if grep -q "^cover:" "$work_dir/metadata.txt"; then # Si ya existe, actualizarlo sed -i.tmp "s|^cover:.*|cover: imagenes/$final_name|" "$work_dir/metadata.txt" rm -f "$work_dir/metadata.txt.tmp" else # Si no existe, añadirlo echo "cover: imagenes/$final_name" >> "$work_dir/metadata.txt" fi log_info "Imagen de portada establecida: imagenes/$final_name" fi done <<< "$image_files" log_success "Procesadas $image_count imágenes" else log_warning "No se encontraron imágenes" fi # Añadir índice al metadata si no está presente if ! grep -q "^index:" "$work_dir/metadata.txt"; then echo "index: index.gmi" >> "$work_dir/metadata.txt" fi # Limpiar contenido EPUB extraído rm -rf "$work_dir/epub_content" # Mostrar resumen final log_success "=== EXTRACCIÓN COMPLETADA ===" log_success "Directorio: $work_dir" log_success "Capítulos originales: $chapter_count" log_success "Capítulos eliminados (en blanco): $capitulos_eliminados" log_success "Capítulos como subtítulos: $capitulos_subtitulo" log_success "Capítulos finales con contenido: $total_capitulos_finales" log_success "Metadatos: metadata.txt" log_success "Índice: index.gmi" echo log_info "Para crear el Gempub, ejecuta:" log_info " $0 -c '$work_dir'" } # Función para validar estructura Gempub validate_gempub_structure() { local gempub_dir="$1" log_info "Validando estructura Gempub..." local errors=0 local warnings=0 # Verificar archivos obligatorios if [ ! -f "$gempub_dir/metadata.txt" ]; then log_error "Falta: metadata.txt" errors=$((errors + 1)) else # Verificar metadatos obligatorios if ! grep -q "title:" "$gempub_dir/metadata.txt"; then log_error "metadata.txt falta 'title'" errors=$((errors + 1)) fi if ! grep -q "gpubVersion:" "$gempub_dir/metadata.txt"; then log_error "metadata.txt falta 'gpubVersion'" errors=$((errors + 1)) fi fi # Verificar archivo índice local index_path="$gempub_dir/index.gmi" if [ -f "$gempub_dir/metadata.txt" ]; then local custom_index=$(grep "^index:" "$gempub_dir/metadata.txt" | cut -d: -f2- | sed 's/^[ \t]*//') if [ -n "$custom_index" ]; then index_path="$gempub_dir/$custom_index" fi fi if [ ! -f "$index_path" ]; then log_error "Falta archivo índice: $index_path" errors=$((errors + 1)) else # Validar enlaces en índice local remote_links=$(grep -E '=> https?://' "$index_path" | grep -v '^#' || true) if [ -n "$remote_links" ]; then log_warning "Enlaces remotos en índice (deberían ser locales):" echo "$remote_links" warnings=$((warnings + 1)) fi fi # Verificar archivos .gmi local gmi_files=$(find "$gempub_dir" -name "*.gmi" | wc -l) if [ "$gmi_files" -eq 0 ]; then log_error "No se encontraron archivos .gmi" errors=$((errors + 1)) fi # Verificar imágenes sin alt-text (según especificación Gempub) local images_no_alt=$(find "$gempub_dir" -name "*.gmi" -exec grep -H '=>.*\.\(jpg\|jpeg\|png\|gif\)[[:space:]]*$' {} \; 2>/dev/null || true) if [ -n "$images_no_alt" ]; then log_warning "Imágenes sin texto alternativo (viola especificación Gempub):" echo "$images_no_alt" warnings=$((warnings + 1)) fi # Resumen de validación if [ $errors -eq 0 ]; then log_success "Validación completada: $warnings advertencias, 0 errores" return 0 else log_error "Validación fallida: $warnings advertencias, $errors errores" return 1 fi } # Función para crear Gempub create_gempub() { local gempub_dir="$1" if [ ! -d "$gempub_dir" ]; then log_error "Directorio no encontrado: $gempub_dir" exit 1 fi # Validar estructura if ! validate_gempub_structure "$gempub_dir"; then log_error "No se puede crear Gempub - estructura inválida" exit 1 fi local base_name=$(basename "$gempub_dir" _gempub) local gempub_file="${base_name}.gpub" log_info "Creando Gempub: $gempub_file" # Crear archivo ZIP con compresión máxima if (cd "$gempub_dir" && zip -qr -9 "../$gempub_file" .); then log_success "Gempub creado exitosamente: $gempub_file" # Mostrar información del archivo local file_size=$(du -h "$gempub_file" | cut -f1) local file_count=$(zipinfo -1 "$gempub_file" | wc -l) log_info " - Tamaño: $file_size" log_info " - Archivos: $file_count" else log_error "Error al crear el archivo Gempub" exit 1 fi } # Procesar argumentos if [ $# -eq 0 ]; then show_help exit 1 fi case "$1" in -e|--extraer) if [ $# -ne 2 ]; then log_error "Se requiere archivo EPUB para extraer" show_help exit 1 fi extract_epub "$2" ;; -c|--crear) if [ $# -ne 2 ]; then log_error "Se requiere directorio para crear Gempub" show_help exit 1 fi create_gempub "$2" ;; -h|--help) show_help ;; *) log_error "Opción no reconocida: $1" show_help exit 1 ;; esac