Navegación

módulos

siguiente |

Inicio »

Manual de usuario de calibre

Manual de usuario de calibre

calibre es un gestor de biblioteca de libros electrónicos. Con calibre puede leer, convertir y catalogar libros electrónicos en la mayoría de los principales formatos. También puede comunicarse con muchos dispositivos de lectura de libros electrónicos. Puede obtener metadatos para los libros de Internet. Puede descargar periódicos y convertirlos en libros electrónicos para una lectura más cómoda. Es multiplataforma, funciona en Linux, Windows y macOS.

Acaba de iniciar calibre. ¿Y ahora qué? Antes de que calibre pueda hacer nada con sus libros electrónicos, tiene que saber de ellos. Arrastre y suelte algunos archivos de libro electrónico en calibre o pulse el botón Añadir libros y seleccione los libros con los que quiere trabajar. Una vez que haya añadido los libros, aparecerán en la ventana principal de manera parecida a ésta:

Cuando haya admirado la lista de los libros que acaba de añadir durante un tiempo suficiente, probablemente quiera leer alguno. Para ello tendrá que convertir el libro a un formato que su lector entienda. La primera vez que ejecute calibre, el Asistente de bienvenida se iniciará y configurará calibre para su dispositivo lector. La conversión es muy sencilla, no tiene más que seleccionar el libro que quiere convertir y pulsar el botón «Convertir libros». Ignore todas las opciones por ahora y pulse «Aceptar». El pequeño icono en la esquina inferior derecha empezará a girar. Cuando deje de girar, el libro convertido está listo. Pulse el botón «Mostrar» para leer el libro.

Si quiere leer el libro en el lector, conecte el lector al equipo, espere a que calibre lo detecte (10-20 segundos) y pulse el botón «Enviar al dispositivo». Cuando el icono deje de girar de nuevo, desconecte el lector y ¡ya puede leer! Si no convirtió el libro en el paso anterior, calibre lo convertirá automáticamente al formato que necesite el dispositivo lector.

Para iniciarse en un uso más avanzado, debería leer sobre La interfaz gráfica de usuario. Si quiere aún más potencia y versatilidad, aprenda Interfaz de línea de órdenes. También puede encontrar útil la lista de Preguntas frecuentes.

Si tiene más dudas, quiere discutir sobre calibre con otros usuarios o pedir ayuda en cuestiones concretas, hay :website:`foros y otros recursos para ayudarle disponibles <help>`_.

Secciones

La interfaz gráfica de usuario

Añadir su sitio de noticias favorito

El visor de libros electrónicos

Conversión de libros

Modificar libros electrónicos

El servidor de contenidos de calibre

Comparar libros electrónicos

Modificar los metadatos de los libros

Preguntas frecuentes

Cursillos

El esquema de URL calibre://

Personalizar calibre

Interfaz de línea de órdenes

Configurar un entorno de desarrollo de calibre

Gestión de derechos digitales (DRM)

Glosario

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

La interfaz gráfica de usuario

La interfaz gráfica de usuario

La interfaz gráfica de usuario (GUI) da acceso a todas las funciones de gestión de biblioteca y conversión de formatos de libro electrónico. El proceso básico para usar calibre comienza añadiendo libros a la biblioteca desde el disco duro. calibre intentará leer automáticamente los metadatos de los libros y los añadirá a la base de datos interna. Una vez que están en la base de datos, puede realizar distintas Acciones sobre ellos, incluidas la conversión de un formato a otro, la transferecencia a un dispositivo de lectura, la visualización en el equipo y la modificación de metadatos. Esta última acción incluye la modificación de la portada, descripción y etiquetas entre otros detalles. Tenga en cuenta que calibre crea copias de los archivos que añada. Los archivos originales no se alteran.

La interfaz se divide en varias secciones:

Acciones

Preferencias

Catálogos

Buscar y ordenar

La interfaz de búsqueda

Guardar búsquedas

Buscar en el texto completo de todos los libros

Bibliotecas virtuales

Marcar libros temporalmente

Extraer metadata de los nombres de archivo

Detalles del libro

Explorador de etiquetas

Cuadro de portadas

Explorador de portadas

Añadir notas para autores, series, etc.

Vista rápida

Tareas

Atajos de teclado

Acciones

La barra de herramientas de acciones ofrece un acceso inmediato a algunas acciones usuales. Si pulsa con el botón derecho sobre los botones puede realizar variaciones de la acción predeterminada. Tenga en cuenta que la barra de herramientas de acciones puede tener un aspecto distinto dependiendo de si hay algún dispositivo de lectura conectado al equipo.

Añadir libros

Editar metadatos

Convertir libros

Mostrar

Enviar al dispositivo

Descargar noticias

Biblioteca

Dispositivo

Guardar en el disco

Conectar y compartir

Eliminar libros

Añadir libros

botón de añadir libros La acción Añadir libros tiene siete variantes accesibles pulsando con el botón derecho sobre el botón.

Añadir libros desde una única carpeta: Abre un diálogo de selección de archivo y le permite especificar qué libros de una carpeta añadir. Esta acción es sensible al contexto, es decir, depende de qué catálogo esté seleccionado. Si está seleccionada la Biblioteca, los libros se añadirán a la biblioteca. Si está seleccionado el dispositivo de lectura, los libros se copiarán al dispositivo, etc.

Añadir libros de carpetas y subcarpetas: Le permite elegir una carpeta. Se buscará en la carpeta y todas sus subcarpetas de manera recursiva y todos los libros que se encuentren se añadirán a la biblioteca. Puede elegir si calibre añadirá todos los archivos presentes en una carpeta a un único registro de libro o a múltiples registros. calibre asume que cada carpeta contiene un único libro y que todos los archivos de libro en la carpeta corresponden al mismo libro en distintos formatos. Esta acción es la inversa de la acción Guardar en el disco, es decir, puede Guardar en el disco, eliminar los libros y volver a añadirlos en el modo de un libro por carpeta, sin pérdida de información excepto por la fecha (suponiendo que no haya modificado ninguna configuración de la acción Guardar en el disco).

Añadir varios libros desde un archivo (ZIP/RAR): Le permite añadir múltiples libros que estén almacenados en los archivos ZIP o RAR seleccionados. Es una vía rápida para evitar tener que descomprimir los archivos primero y luego añadir los libros con alguna de las dos opciones anteriores.

Añadir libro en blanco (entrada de libro sin ningún formato): Le permite añadir un registro de libro en blanco. Esto se puede usar para después rellenar manualmente la información de un libro que quizá aún no tenga en la colección.

Añadir a partir del ISBN: Le permite añadir uno o más libros introduciendo sus ISBN.

Añadir archivos a los registros de libros seleccionados: Le permite añadir o actualizar los archivos asociados con un libro existente en la biblioteca.

Añadir archivos de datos a los registros de libros seleccionados: Le permite añadir cualquier cantidad de archivos adicionales que se almacenarán en un subdirectorio data en el directorio del libro. Ver Añadir archivos de datos adicionales a un libro para más detalles.

Añadir un archivo vacío a los registros de libros seleccionados: Le permite añadir un archivo vacío del formato especificado a los registros de libros seleccionados.

La acción Añadir libros puede leer metadatos de una amplia variedad de formatos de libro electrónico. Además, intenta extraer metadatos a partir del nombre de archivo. Vea la sección Extraer metadata de los nombres de archivo para saber cómo configurar esto.

Para añadir un formato adicional a un libro existente, puede hacer cualquiera de estas tres cosas:

Arrastrar y soltar un archivo en el panel de detalles del libro, en la parte derecha de la ventana principal.

Pulse con el botón derecho sobre Añadir libros y elija Añadir archivos a los registros de libros seleccionados.

Pulsar sobre el Añadir libros en la parte superior derecha del diálogo Modificar metadatos, accesible mediante la acción Editar metadatos.

Editar metadatos

botón de modificar metadatos La acción Modificar metadatos tiene cuatro variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Modificar metadatos por separado: Le permite modificar los metadatos de los libros uno a uno, con la opción de obtener los metadatos, incluidas las portadas, de Internet. También le permite añadir o eliminar formatos específicos de un libro.

Modificar metadatos en masa: Le permite modificar campos de metadatos comunes a un gran número de libros simultáneamente. La operación se realiza sobre todos los libros que estén seleccionados en la Vista de biblioteca.

Descargar metadatos y portadas: Descarga metadatos y portadas (si están disponibles) para los libros seleccionados en la lista de libros.

Unir registros de libros: Le ofrece la posibilidad de unir los metadatos y formatos de dos o más registros de libros. Puede elegir borrar o mantener los registros distintos del primero seleccionado.

Administrar archivos de datos: Administrar los archivos de datos adicionales asociados con los libros seleccionados.

Para más detalles, ver Modificar los metadatos de los libros.

Convertir libros

botón de convertir libros Los libros electrónicos pueden convertirse de una variedad de formatos al formato que su lector de libros electrónicos prefiera. Muchos libros electrónicos a la venta estarán protegidos por tecnología de gestión digital de derechos <drm> (DRM). calibre no convertirá estos libros. Es sencillo eliminar la DRM de muchos formatos, pero como esto puede ser ilegal, es su responsabilidad encontrar las herramientas para liberar sus libros y luego usar calibre para convertirlos.

Para la mayoría de los usuarios, la conversión es cuestión de una sola pulsación. Si quiere saber más sobre el proceso de conversión, vea Conversión de libros.

La acción Convertir libros tiene tres variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Convertir por separado: Le permite especificar opciones de conversión para personalizarla conversión de cada libro seleccionado.

Convertir en masa: Le permite especificar opciones una sola vez y convertir un número de libros en masa.

Crear un catálogo de los libros en la biblioteca de calibre: Le permite generar un listado completo de los libros en la biblioteca, incluyendo todos los metadatos, en formatos como XML, CSV, BiBTeX, EPUB y MOBI. El catálogo contendrá todos los libros que se muestren actualmente en la vista de biblioteca. Esto le permite usar las funciones de búsqueda para limitar los libros catalogados. Además, si selecciona varios libros con el ratón, sólo se incluirán esos libros. Si genera el catálogo en un formato de libro electrónico como EPUB, MOBI o AZW3, la próxima vez que conecte un dispositivo de lectura el catálogo se enviará automáticamente al dispositivo. Para más información sobre cómo funcionan los catálogos, lea Crear catálogos AZW3 • EPUB • MOBI.

Mostrar

botón de mostrar libro La acción Mostrar abre el libro en un programa visualizador de libros electrónicos. calibre tiene un visor incorporado para muchos formatos de libro electrónico. Para otros formatos usa la aplicación predeterminada del sistema operativo. Puede configurar qué formatos se abren con el visor interno en Preferencias > Interfaz > Comportamiento. Si un libro tiene más de un formato, puede abrir un formato específico pulsando con el botón derecho sobre el botón de la acción.

Enviar al dispositivo

botón de enviar al dispositivo La acción Enviar al dispositivo tiene ocho variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Enviar a la memoria principal: Los libros seleccionados se copian en la memoria principal del dispositivo de lectura.

Enviar a la tarjeta de memoria A: Los libros seleccionados se copian a la tarjeta de memoria (A) en el dispositivo de lectura.

Enviar a la tarjeta de memoria B: Los libros seleccionados se copian a la tarjeta de memoria (B) en el dispositivo de lectura.

Enviar formato específico a: Los libros seleccionados se copian al lugar de almacenamiento seleccionado en el dispositivo, en el formato que especifique.

Desconectar dispositivo. Desconecta el dispositivo de calibre.

Asignar acción predeterminada de enviar al dispositivo: Le permite especificar cuál de las acciones (1 a 5, o 7) será la acción predeterminada cuando se pulsa el botón principal.

Enviar y eliminar de la biblioteca: Los libros seleccionados se copian al lugar de almacenamiento seleccionado en el dispositivo y despues se borran de la biblioteca.

Obtener anotaciones (experimental): Copia las anotaciones que pueda haber en un libro del dispositivo al campo de comentarios en los metadatos del libro en la biblioteca de calibre.

Puede controlar el nombre de archivo y la estructura de carpetas de los archivos que se envían al dispositivo estableciendo una plantilla en Preferencias > Importar y exportar > Enviar libros a dispositivos. Véase también El lenguaje de plantillas de calibre.

Descargar noticias

botón de obtener noticias La acción:Obtener noticias descarga noticias de distintos sitios web y las convierte en un libro electrónico que pued leer en un dispositivo de lectura. Normalmente, el libro electrónico creado se añade a la biblioteca, pero si tiene un dispositivo conectado cuando termina la descarga, el libro también se copia al lector automáticamente.

La acción Obtener noticias usa fórmulas simples (10-15 líneas de código) por cada sitio de noticias. Para saber cómo crear fórmulas para sus fuentes de noticias preferidas, vea Añadir su sitio de noticias favorito.

La acción Obtener noticias tiene tres variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Planificar descarga de noticias: Le permite planificar la descarga de las fuentes de noticias seleccionadas de los cientos disponibles. La planificación puede establecerse de manera individual para cada fuente de noticias seleccionada y permite seleccionar días de la semana específicos o un número de días de intervalo entre descargas.

Añadir una nueva fuente de noticias: Le permite crear una fórmula simple para descargar noticias de un sitio de noticias personalizado al que quiera acceder. Crear una fórmula puede ser tan simple como especificar la dirección URL de un canal RSS, o puede ser más detallado, creando un código basado en python para la tarea. Para más información, véase Añadir su sitio de noticias favorito.

Descargar todas las fuentes de noticias planificadas: Hace que calibre empiece inmediatamente a descargar todas las fuentes de noticias que tenga planificadas.

Biblioteca

botón de biblioteca La acción Biblioteca le permite crear, eliminar o cambiar el nombre a una biblioteca, o cambiar de una biblioteca a otra. calibre le permite crear tantas bibliotecas como desee. Puede, por ejemplo, crear una biblioteca de ficción, una de no ficción, una de idiomas extranjeros, una de proyectos, o cualquier estructura que le convenga. Las bibliotecas son la estructura de organización de mayor nivel dentro de calibre. Cada biblioteca tiene su propio conjunto de etiquetas, categorías y ubicación de almacenamiento base.

Cambiar o crear biblioteca…: Le permite: a) conectar con una biblioteca de calibre existente en otra ubicación, b) crear una biblioteca vacía en una nueva ubicación o c) mover la biblioteca actual a una nueva ubicación.

Cambio rápido: Le permite cambiar de una biblioteca a otra si han sido registradas o creadas con calibre.

Cambiar el nombre a la biblioteca: Le permite cambiar el nombre a una biblioteca.

Elegir un libro aleatorio: Elige un libro aleatorio de la biblioteca

Eliminar biblioteca: Le permite dar de baja una biblioteca en calibre.

Exportar o importar todos los datos de calibre: Le permite exportar los datos de calibre para migrar a un nuevo equipo o importar los datos previamente exportados.

<nombre de biblioteca>: Las acciones 7, 8, etc. le dan acceso inmediato a las bibliotecas que haya creado o registrado. Esta lista contiene sólo las 5 bibliotecas más usadas. Para tener la lista completa, use el menú Cambio rápido.

Mantenimiento de la biblioteca: Le permite comprobar la coherencia de los datos en la biblioteca actual, detectar problemas y restaurar la base de datos de la biblioteca a partir de copias de respaldo.

Nota

Los metadatos de los libros, como el título, autor y etiquetas, se almacenan en un único archivo en la carpeta de la biblioteca de calibre, llamado metadata.db. Si este archivo se corrompe (un suceso muy infrecuente) pueden perderse los metadatos. Por suerte, calibre copia automáticamente los metadatos de cada libro en su correspondiente carpeta como un archivo OPF. Mediante la acción Restaurar base de datos bajo Mantenimiento de la biblioteca, descrita anteriormente, puede hacer que calibre reconstruya el archivo metadata.db a partir de los archivos OPF individuales.

Puede copiar o mover libros entre diferentes bibliotecas (una vez que haya configurado más de una biblioteca) pulsando con el botón derecho sobre un libro y seleccionando la acción Copiar a biblioteca.

Dispositivo

botón de dispositivo La acción Dispositivo le permite ver los libros en la memoria principal o en las tarjetas de almacenamiento de un dispositivo o desconectarlo de calibre. Este icono aparece automáticamente en la barra de herramientas principal de calibre cuando se conecta un dispositivo soportado. Puede pulsar sobre él para ver los libros en el dispositivo. También puede arrastrar y soltar libros de la biblioteca de calibre sobre el icono para copiarlos al dispositivo. Análogamente, puede arrastrar y soltar libros del dispositivo sobre el icono de la biblioteca en la barra de herramientas para copiarlos del dispositivo a la biblioteca de calibre.

Guardar en el disco

botón de guardar en disco La acción Guardar en el disco tiene cinco variantes, accesibles pulsando con el botón derecho sobre el botón de la acción.

Guardar en el disco: Guarda los libros seleccionados en el disco, organizados por carpetas. La estructura de carpetas es algo así:

Author_(sort)
    Title
        Book Files

Puede controlar el nombre de archivo y la estructura de carpetas de los libros guardados en el disco estableciendo una plantilla en Preferencias > Importar y exportar > Guardar libros en disco. Véase también El lenguaje de plantillas de calibre.

Guardar en el disco, en una única carpeta: Guarda los libros seleccionados en el disco, en una única carpeta.

Con las opciones 1 y 2 se guardan en el disco todos los formatos, incluidos los metadatos, de cada libro seleccionado. Los metadatos se almacenan en un archivo OPF. Los libros guardados pueden volver a ser importados a la biblioteca sin pérdida de información usando la acción Añadir libros.

Guardar solamente el formato *<principal>* en disco: Guarda los libros seleccionados en el disco con la estructura de carpetas mostrada en (1), pero sólo en el formato principal. Puede establecer el formato preferido en Preferencias > Interfaz > Comportamiento > Formato de salida principal.

Guardar sólo el formato *<principal>* en una única carpeta: Guarda los libros seleccionados en el disco, en una única carpeta, pero sólo en el formato principal.

Guardar un solo formato en disco…: Guarda los libros seleccionados en el disco con la estructura de carpetas mostrada en (1), pero sólo en el formato que seleccione en la lista emergente.

Conectar y compartir

botón de conectar y compartir La acción Conectar y compartir le permite conectar manualmente con un dispositivo o carpeta en el equipo. También le permite configurar la biblioteca de calibre para que pueda accederse a través de un navegador web o por correo electrónico.

La acción Conectar y compartir tiene cuatro variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Conectar a carpeta: Le permite conectar con cualquier carpeta en el equipo como si fuera un dispositivo y usar las funciones que calibre ofrece para los dispositivos con esa carpeta. Esto es útil si tiene un dispositivo que no está soportado por calibre pero que está disponible como un disco USB.

Iniciar servidor de contenido: Inicia el servidor web incorporado en calibre. Cuando se ha iniciado, la biblioteca de calibre será accesible mediante un navegador web a través de Internet (si quiere que así sea). Puede configurar cómo se accede al servidor web en Preferencias > Compartir > Compartir por la red.

Configurar el intercambio de libros por correo electrónico: Le permite compartir libros y canales de noticias por correo electrónico. Después de configurar las direcciones de correo para esta opción, calibre enviará actualizaciones de noticias y de libros a las direcciones suministradas. Puede configurar cómo se envían los correos electrónicos en Preferencias > Compartir > Compartir libros por correo electrónico. Una vez que haya configurado una o varias direcciones de correo, esta entrada del menú se sustituirá por entradas para enviar libros a las direcciones configuradas.

Eliminar libros

botón de eliminar libros La acción Eliminar libros elimina libros permanentemente, así que úsela con cuidado. Es sensible al contexto, es decir, su comportamiento depende de qué catálogo tenga seleccionado. Si tiene seleccionada la Biblioteca, los libros se eliminarán de la biblioteca. Si tiene seleccionado el dispositivo de lectura, los libros se eliminarán del dispositivo. Para eliminar un formato específico de un libro use la acción Editar metadatos. Eliminar libros también tiene seis variantes accesibles pulsando con el botón derecho sobre el menú de la acción.

Eliminar libros seleccionados: Le permite eliminar permanentemente todos los libros seleccionados en la lista de libros

Eliminar archivos de un formato específico de los libros seleccionados…: Le permite eliminar permanentemente archivos de un formato determinado de los libros seleccionados en la lista de libros.

Eliminar todos los formatos de los libros seleccionados, excepto…: Le permite eliminar permanentemente los archivos de libro de cualquier formato excepto el especificado de los libros seleccionados en la lista de libros.

Eliminar todos los formatos de los libros seleccionados: Le permite eliminar permanentemente todos los archivos de libro de los libros seleccionados en la lista de libros. Sólo quedarán los metadatos.

Eliminar portadas de los libros seleccionados: Le permite eliminar permanentemente los archivos de imagen de portada de los libros seleccionados en la lista de libros.

Eliminar los libros correspondientes del dispositivo: Le permite eliminar de un dispositivo conectado los archivos de libro que correspondan a los libros seleccionados en la lista de libros.

Restaurar eliminados recientemente: Le permite deshacer el borrado de libros o formatos.

Nota

Tenga en cuenta que al usar Eliminar libros para borrar libros de la biblioteca de calibre, el registro del libro se elimina, pero los libros se almacenan temporalmente, por algunos días, en una carpeta de papelera. Puede deshacer el borrado pulsando con el botón derecho sobre Eliminar libros y eligiendo Restaurar eliminados recientemente.

Preferencias

botón de preferencias La acción Preferencias le permite cambiar la manera en que funcionan distintos aspectos de calibre. Tiene cuatro variantes accesibles pulsando con el botón derecho sobre el botón de la acción.

Preferencias: Le permite cambiar la manera en que funcionan los distintos aspectos de calibre. Esta acción también se activa simplemente pulsando sobre el botón.

Ejecutar el asistente de bienvenida: Le permite iniciar el Asistente de bienvenida, que apareció la primera vez que se ejectuó calibre.

Obtener complementos para mejorar calibre: Abre una nueva ventana que muestra complementos para calibre. Estos complementos están desarrollados por terceros para extender las funciones de calibre.

Reiniciar en el modo de depuración: Le permite activar un modo de depuración, que puede ayudar a los desarrolladores de calibre a resolver los problemas que pueda encontrar con el programa. Para la mayoría de los usuarios esta opción debe permanecer desactivada, a menos que un desarrollador le indique que la active.

Catálogos

Un catálogo es una colección de libros. calibre puede gestionar dos tipos diferences de catálogos:

Biblioteca: Es una colección de libros almacenados en la biblioteca de calibre en el equipo.

Dispositivo: Es una colección de libros almacenados en un dispositivo de lectura. Estará disponible cuando conecte el dispositivo al equipo.

Muchas operaciones, como añadir o eliminar libros, visualizarlos, etc., son sensibles al contexto. Así, por ejemplo, si pulsa el botón Mostrar cuando tiene el catálogo Dispositivo seleccionado, calibre abrirá los archivos en el dispositivo para mostrarlos. Si tiene el catálogo Biblioteca seleccionado, se abrirán los archivos en la biblioteca de calibre.

Buscar y ordenar

La sección de búsqueda y ordenación le permite realizar varias acciones muy útiles sobre las colecciones de libros.

Puede ordenarlas por título, autor, fecha, calificación, etc. pulsando sobre los títulos de las columnas. También puede ordenar por varias columnas (ordenación secundaria). Por ejemplo, si pulsa sobre la columna título y luego sobre la columna autor, los libros se ordenarán por autor y todas las entradas del mismo autor estarán ordenadas por título.

Puede buscar un libro determinado o un conjunto de libros usando la barra de búsqueda. Más adelante hay más información sobre búsquedas.

Puede modificar metadatos de manera rápida y cómoda seleccionando la entrada que quiere modificar en la lista y pulsando la tecla E.

Puede realizar Acciones sobre conjuntos de libros. Para seleccionar varios libros tiene varias opciones:

Mantener pulsada la tecla Ctrl y pulsar sobre los libros que quiera seleccionar.

Mantener pulsada la tecla Mayús y pulsar sobre el primer y último libro de un grupo que quiera seleccionar.

Puede configurar qué campos se muestran usando el diálogo Preferencias.

Para realizar una subclasificación compleja basada en varias columnas, añada la herramienta Ordenar por a una barra de herramientas a través de Preferencias > Barras de herramientas y menús.

La interfaz de búsqueda

Puede buscar en todos los metadatos de los libros introduciendo los términos de búsqueda en la barra de búsqueda. Por ejemplo:

Asimov Foundation format:lrf

Esto encontrará todos los libros en la biblioteca que tengan «Asimov» y «Foundation» en los metadatos y que estén disponibles en formato LRF. Otros ejemplos:

author:Asimov and not series:Foundation
title:"The Ring" or "This book is about a ring"
format:epub publisher:feedbooks.com

Tipos de búsqueda

Hay cuatro tipos de búsqueda: «contiene», «igualdad», «expresión regular» (ver expresión regular <https://es.wikipedia.org/wiki/Expresi%C3%B3n_regular>_) y `variante de caracteres. Puede elegir el tipo de búsqueda mediante un carácter de prefijo.

Búsquedas de tipo «contiene»

Las búsquedas sin carácter de prefijo son de tipo «contiene» y de manera predeterminada no distinguen mayúsculas y minúsculas. Un elemento coincide si el texto de búsqueda aparece en cualquier lugar de los metadatos indicado. Puede hacer que las búsquedas de tipo «contiene» distingan mayúsculas y minúsculas activando la opción Búsqueda con distinción de mayúsculas y minúsculas en Preferencias > Búsqueda. Si la opción de búsqueda Los caracteres sin acentos coinciden con los caracteres acentuados está marcada, un carácter determinado coincidirá con todas sus variantes (es decir, e coincidirá con é, è, ê y ë) y todos los espacios y signos de puntuación se ignoran. Por ejemplo, si la opción Los caracteres sin acentos coinciden… está marcada, entonces, dados los dos títulos:

Grande, feo y malo

El gran defecto mágico

estas búsquedas encontrarán:

title:"ma" coincide con ambos (a coincide con a y á).

title:"de " coincide con ambos porque los espacios se ignoran.

title:"de," coincide con ambos porque la coma se ignora.

title:"def" coincide con ambos porque , y los espacios se ignoran en el libro 1.

title:"de f" coincide con ambos por la coma y el espacio se ignoran.

title:"ym" coincide con el libro 1 porque el espacio en y malo se ignora.

title:"," coincide con ambos (en realidad con todos los libros) porque las comas se ignoran.

Si la opción Los caracteres sin acentos coinciden… no está marcada, las variantes de caracteres, los espacios y los signos de puntuación son todos significativos.

Sólo puede activarse una de las opciones Búsqueda con distinción de mayúsculas y minúsculas y Los caracteres sin acentos coinciden con los caracteres acentuados.

Búsquedas de tipo «igualdad»

Las búsquedas de tipo «igualdad» se indican añadiendo al texto de búsqueda un signo igual (=) como prefijo. Por ejemplo, la consulta tag:"=ciencia" coincidirá con la etiqueta ciencia, pero no ciencia ficción o ciencia dura. Las variantes de caracteres son significativas: é no coincide con e.

Se usan dos variantes de búsqueda de tipo «igualdad» para los elementos jerárquicos (como A.B.C): búsquedas de prefijo jerárquico y búsquedas de componente jerárquico. La primera, indicada mediante un único punto tras el signo igual (=.) coincide con las partes iniciales de un elemento jerárquico. La segunda, indicada mediante dos puntos tras el signo igual (=..) coincide con un nombre interno en el elemento jerárquico. Ejemplos, usando la etiqueta Historia.Militar.GM2 como el valor:

tags:"=.Historia": coincide. Historia es un prefijo de la etiqueta.

tags:"=.Historia.Militar": coincide. Historia.Militar es un prefijo de la etiqueta.

tags:"=.Historia.Militar.GM2": coincide. Historia.Militar.GM2 es un prefijo de la etiqueta, aunque uno impropio.

tags:"=.Militar": no coincide. Militar no es un prefijo de la etiqueta.

tags:"=.GM2": no coincide. GM2 no es un prefijo de la etiqueta.

tags:"=..Historia": coincide. La jerarquía contiene el valor Historia.

tags:"=..Militar": coincide. La jerarquía contiene el valor Militar.

tags:"=..GM2": coincide. La jerarquía contiene el valor GM2.

tags:"=..Militar.GM2": no coincide. La búsqueda .. sólo encuentra valores individuales.

Búsquedas de tipo «expresión regular»*

Las búsquedas de tipo «expresión regular» se indican precediendo el texto de búsqueda con una tilde (~). Puede usarse cualquier expresión regular compatible con Python [https://docs.python.org/library/re.html]. Las barras invertidas usadas para incluir caracteres especiales en las expresiones regulares deben duplicarse, porque las barras invertidas aisladas se eliminan al procesar las búsquedas. Por ejemplo, para encontrar un paréntesis literal debe introducir \\( o bien usar las supercomillas (ver más adelante). Las búsquedas con expresiones regulares son de tipo «contiene» a no ser que la expresión esté anclada. Las variantes de caracteres son significativas: ~e no coincide con é.

Búsquedas de tipo «variante de carácter»

Las búsquedas de tipo «variante de carácter» se indican añadiendo como prefijo al texto de búsqueda un circunflejo (^). Esta búsqueda es similar a la de tipo «contiene» (ver más arriba), excepto que:

no se distinguen mayúsculas y minúsculas.

las variantes de carácter siempre coinciden entre sí.

los espacios y signos de puntuación siempre son significativos.

Las opciones de búsqueda Los caracteres sin acentos coinciden con los caracteres acentuados y Búsqueda con distinción de mayúsculas y minúsculas se ignoran. No tienen efecto en el comportamiento de este tipo de búsqueda.

A continuación se compara esta búsqueda con una de tipo «contiene», suponiendo que la opción Los caracteres sin acentos coinciden… está marcada (ver más arriba), dados los mismos dos títulos:

Grande, feo y malo

El gran defecto mágico

estas búsquedas de tipo «variante de carácter» encuentran:

title:"^ma" coincide con ambos (a coincide con a y á)

title:"^de" coincide con ambos

title:"^def" coincide con el libro 2

title:"^de," coincide con el libro 1 porque la coma es significativa

title:"^de " no coincide con ninguno, porque tanto el espacio como la coma son significativos

title:"^defe" coincide con el libro 2

title:"^ym" no coincide con nada

title:"^," coincide con el libro 1 (en lugar de todos los libros) porque la coma es significativa

Sintaxis de expresiones regulares

Una expresión de búsqueda es una secuencia de términos de búsqueda separados opcionalmente por los operadores and (y) y or (o). Si entre dos términos de búsqueda no hay operador de separación, se supone and. El operador and tiene prioridad sobre or, por ejemplo la expresión a or b and c es igual que a or (b and c). Puede usar paréntesis para cambiar la prioridad, por ejemplo (a or b) and c) para hacer que el or se evalúe antes que el and. Puede usar el operador not (no) para negar o invertir el resultado de evaluar una expresión de búsqueda. Ejemplos:

not tag:blabla encuentra todos los libros que no contienen la etiqueta blabla

not (author:Asimov or author:Weber) encuentra todos los libros cuyo autor no es ni Asimov ni Weber.

Los ejemplos anteriores muestran ejemplos de términos de búsqueda. Un término de búsqueda básico es una secuencia de caracteres que no incluyen espacios, comillas ("), barras invertidas (\) o paréntesis (( )). Puede ir opcionalmente precedida de un especificador de nombre de columna: el nombre de consulta de una columna seguido de dos puntos (:). Por ejemplo author:Asimov. Si el término de búsqueda debe contener un espacio, el término completo ha de ir entre comillas, como en title:"El anillo". Si el término de búsqueda debe contener comillas, éstas han de escaparse con barras invertidas. Por ejemplo, para buscar una serie llamada The «Ball» and The «Chain», use:

series:"The \"Ball\" and The \"Chain\"

Si necesita usar una barra invertida literal, algo que ocurre frecuentemente en búsquedas de expresión regular, use dos de ellas (\\).

A veces es difícil poner todas las secuencias de escape correctamente para que el resultado sea el deseado, especialmente en búsquedas de expresión regular y plantilla. En estos casos, use las supercomillas """secuencia de caracteres""". Los caracteres entre supercomillas se usan sin cambios, no se procesan los escapes.

Más información

Para buscar un texto que empiece por un signo igual, una tilde o un circunflejo, añada una barra invertida (\) como prefijo.

Escriba los textos de búsqueda entre comillas (") si el texto contiene paréntesis o espacios. Por ejemplo, para buscar libros con la etiqueta Ciencia ficción deberá escribir tag:"=ciencia ficción". Si escribe tag:=ciencia ficción encontrará todos los libros con la etiqueta «ciencia» y con la palabra «ficción» en cualquier metadato.

Puede construir búsquedas avanzadas de manera sencilla usando el Diálogo de búsqueda avanzada, al que se accede pulsando el botón botón de búsqueda a la izquierda del cuadro de búsqueda.

Los campos disponibles para búsquedas son: tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl y las columnas personalizadas. Si está conectado algún dispositivo, el campo ondevice está también disponible para buscar en la vista de biblioteca de calibre. Para conocer el nombre de búsqueda de una columna personalizada (llamado nombre de consulta), ponga el cursor del ratón sobre el encabezado de la columna en la vista de biblioteca.

Fechas

La sintaxis para buscar fechas es:

pubdate:>2000-1 Will find all books published after Jan, 2000
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
pubdate:=2009 Will find all books published in 2009

Si la fecha es ambigua, la configuración local actual se usa para la comparación de fechas. Por ejemplo, con una configuración mm/dd/yyyy, 2/1/2009 se interpreta como 1 Feb 2009; con una configuración dd/mm/yyyy, se interpreta como 2 Ene 2009. Hay algunas palabras especiales disponibles. El texto hoy significa la fecha de hoy, sea la que sea. Los textos ayer y estemes (o sus equivalentes traducidos al idioma actual) también funcionan. Además, el texto díasatrás (también traducido) puede usarse para indicar una fecha de hace un cierto número de días. Por ejemplo:

date:>10daysago
date:<=45daysago

Para evitar problemas potenciales con textos traducidos al usar versiones de calibre en idiomas distintos del inglés, los textos _today, _yesterday, _thismonth y _daysago están siempre disponibles. No están traducidos.

Buscar fechas y valores numéricos con comparaciones relativas

Los campos numéricos y de fecha permiten usar los operadores de relación = (igual), > (mayor), >= (mayor o igual), < (menor), <= (menor o igual) y != (distinto). Los campos de calificación se consideran numéricos. Por ejemplo, la búsqueda rating:>=3 encontrará todos los libros con una calificación de 3 o más.

Puede buscar libros que tengan un formato de cierto tamaño de esta manera:

size:>1.1M encontrará libros con algún formato de tamaño mayor de 1.1 MB

size:<=1K encontrará libros con algún formato de tamaño menor o igual que 1 KB

Puede realizar una búsqueda por el número de elementos en campos de valor múltiple, como las etiquetas, usando el carácter #, y después la misma sintaxis que en los campos numéricos. Por ejemplo, para buscar todos los libros con más de cuatro etiquetas, use tags:#>4. Para buscar todos los libros con exactamente diez etiquetas, use tags:#=10.

Números de serie

Los números de serie se pueden buscar. Para las series normales, el nombre de búsqueda es series_index. Para columnas personalizadas de serie, use el nombre de búsqueda de la columna seguido de _index. Por ejemplo, para buscar los números de una columna personalizada de serie llamada #mi_serie, se usaría el nombre de búsqueda #mi_serie_index. Los números de serie son números, por lo que se pueden usar los operadores de relación descritos anteriormente.

Búsquedas guardadas

El campo especial search se usa para las :ref`búsquedas guardadas <saved_searches>`. Si guarda una búsqueda con el nombre «Los libros de mi cónyuge», puede introducir search:"Los libros de mi cónyuge" en la barra de búsqueda para reutilizar la búsqueda guardada. Más adelante hay más información sobre el guardado de búsquedas.

Bibliotecas virtuales

El campo especial vl se utiliza para buscar libros en una biblioteca virtual. Por ejemplo, vl:Leído encontrará todos los libros en la biblioteca virtual Leído. La búsqueda vl:Leído y vl:"Ciencia ficción" encontrará todos los libros que están simultáneamente en las bibliotecas virtuales Leído y Ciencia ficción. El valor que sigue a vl: debe ser el nombre de una biblioteca virtual. Si el nombre de la biblioteca virtual contiene espacios, póngalo entre comillas.

Si un campo tiene un valor o está vacío

Puede buscar libros que contengan o no un valor en un campo usando «true» y «false». Por ejemplo:

cover:false encuentra todos los libros sin portada

series:true encuentra todos los libros que pertenecen a alguna serie

series:false encuentra todos los libros que no pertenecen a una serie

comments:false encuentra todos los libros con comentarios vacíos

formats:false encuentra todos los libros sin archivos de libro (registros vacíos)

Columnas personalizadas de sí o no

Si busca «false», «vacío» o «blanco» en una columna personalizada de sí o no, se encontrarán todos los libros con valor indefinido en la columna. Si busca «true» se encontrarán todos los libros que no tengan un valor indefinido. Si busca «sí» o «marcado» se encontrarán todos los libros con Sí en la columna. Si busca «no» o «desmarcado» se encontrarán todos los libros con No en la columna. Tenga en cuenta que las palabras sí, no, blanco, vacío, marcado y desmarcado están traducidas; puede usar tanto el equivalente en el idioma actual o la palabra inglesa. Las palabras true y false y los valores especiales _yes, _no y _empty no están traducidos.

Identificadores

Los identificadores (ISBN, DOI, LCCN, etc.) usan una sintaxis extendida. Un identificador tiene la forma tipo:valor, como en isbn:123456789. La sintaxis extendida le permite especificar de manera independiente el tipo y valor a buscar. Tanto la parte del tipo como la del valor pueden usar cualquiera de los :ref`tipos de búsqueda <search_kinds>`. Ejemplos:

identifiers:true encontrará libros con cualquier identificador.

identifiers:false encontrará libros sin identificador.

identifiers:123 encontrará libros con cualquier tipo de identificador cuyo valor contenga 123.

identifiers:=123456789 encontrará libros con cualquier tipo de identificador cuyo valor sea igual a 123456789.

identifiers:=isbn: e identifiers:isbn:true encontrarán libros con identificador de tipo ISBN y cualquier valor.

identifiers:=isbn:false encontrará libros sin identificador de tipo ISBN.

identifiers:=isbn:123 encontrará libros con identificador de tipo ISBN cuyo valor contenga 123.

identifiers:=isbn:=123456789 encontrará libros con identificador de tipo ISBN cuyo valor sea igual a 123456789.

identifier:i:1 encontrará libros con identificadores cuyo tipo contenga una i y cuyo valor contenga un 1.

Categorías visibles en el explorador de etiquetas

La búsqueda in_tag_browser:true` encuentra todos los libros que están en categorías (etiquetas, autores, etc.) mostradas actualmente en el Explorador de etiquetas. Esto es útil si activa las dos opciones Preferencias > Apariencia > Explorador de etiquetas > Ocultar categorías vacías y Buscar muestra todos los elementos que coinciden. Con esas dos opciones activadas, una Búsqueda en el Explorador de etiquetas muestra sólo las categorías que contienen elementos que coinciden con la Búsqueda. Entonces la búsqueda in_tag_browser:true también encuentra libros con estas categorías o elementos. Puede realizar esta búsqueda rápidamente mediante el atajo Ctrl+Alt+Mayús+F o pulsando el botón de configuración en el Explorador de etiquetas y eligiendo la entrada Mostrar sólo los libros que tienen categorías visibles.

Búsqueda usando plantillas

Puede buscar usando una plantilla en El lenguaje de plantillas de calibre en lugar de un campo de metadatos. Para ello debe introducir una plantilla, un tipo de búsqueda y un valor para buscar. La sintaxis es:

template: (the template) #@#: (search type) : (the value)

template es cualquier plantilla válida del lenguaje de plantillas de calibre. search type debe ser uno de t (búsqueda de texto), d (búsqueda de fecha), n (búsqueda numérica), b (establecido o no). value es el valor que desea, y puede usar los tipos de búsqueda descritos anteriormente para los distintos tipos de búsqueda. Debe poner todo el texto de búsqueda entre comillas si contiene algún espacio.

Ejemplos:

template:"program: connected_device_name('main')#@#:t:kindle" – es «true» cuando el dispositivo kindle está conectado.

template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – encuentra libros con archivo EPUB mayores de 1 MB.

template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – encuentra libros con archivo epub de antigüedad menor de 10 días.

template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" – encuentra todos los libros que contienen el nombre de la serie en las etiquetas. Este ejemplo usa supercomillas porque la plantilla emplea tanto comillas simples (') como dobles (") para construir la expresión de búsqueda.

Puede construir consultas de búsqueda por plantilla fácilmente usando el Diálogo de búsqueda avanzada, al que se accede pulsando el botón botón de búsqueda. Puede probar plantillas en libros concretos con la Prueba de plantillas, que puede añadirse a las barras de herramientas o menús en Preferencias > Barras de herramientas y menús, o asignarse a un atajo de teclado en Preferencias > Atajos.

Diálogo avanzado de búsqueda

Diálogo de búsqueda avanzada

Guardar búsquedas

calibre le permite guardar una búsqueda usada frecuentemente con un nombre especial, y luego usar dicha búsqueda con una sola pulsación. Para ello, cree la búsqueda escribiendo en la barra de búsqueda o con el explorador de etiquetas. Después escriba el nombre que desee darle a la búsqueda en el cuadro de búsquedas guardadas junto a la barra de búsqueda. Pulse el icono con el signo más junto al cuadro de búsquedas guardadas para guardar la búsqueda.

A partir de ahora podrá acceder a la búsqueda guardada en el explorador de etiquetas, bajo Búsquedas guardadas. Una sola pulsación le permite volver a usar fácilmente búsquedas de cualquier complejidad, sin tener que crearlas de nuevo.

Buscar en el texto completo de todos los libros

ftsb calibre puede de manera opcional indexar el texto completo de los libros de la biblioteca para permitir la búsqueda instantánea de palabras dentro de cualquier libro. Para usar esta funcionalidad pulse el botón FT en el borde izquierdo de la barra de búsqueda.

Después, active el indexado para la biblioteca actual actual. Una vez completado el indexado, puede buscar en el texto de toda la biblioteca. Cuando añada nuevos libros a la biblioteca se indexarán automáticamente en segundo plano. Esta búsqueda le permite encontrar rápidamente todos los libros que contengan una palabra o combinación de palabras. Puede incluso buscas palabras que aparecen cerca de otras palabras, como se muestra en los ejemplos de la ventana emergente de búsqueda. Tenga en cuenta que esta herramienta de búsqueda sólo encontrará una coincidencia de la consulta de búsqueda en un libro determinado, no dará una lista de todas las coincidencias, para ello es mejor buscar dentro del libro con el visor de libros electrónicos de calibre.

Puede reindexar un libro individual pulsando con el botón derecho sobre el panel de detalles del libro en calibre y eligiendo Reindexar este libro para búsqueda de texto completo.

Bibliotecas virtuales

La opción Biblioteca virtual es una manera de que calibre muestre sólo algunos libros en vez de contener la biblioteca completa. Ésta es una manera excelente de dividir la biblioteca completa en pequeñas secciones fáciles de manejar. Para aprender cómo crear y usar bibliotecas virtuales, vea el cursillo Bibliotecas virtuales.

Marcar libros temporalmente

Puede marcar temporalmente conjuntos arbitrarios de libros. Los libros marcados aparecerán con una chincheta y pueden encontrarse con la búsqueda marked:true. Para marcar un libro pulse Ctrl+M o vaya a Preferencias > Barras de herramientas y menús y añada el botón Marcar libros a la barra de herramientas principal.

Puede marcar libros con un texto específico pulsando con el botón derecho sobre el botón Marcar libros y eligiendo Marcar libros con etiqueta de texto. Los libros marcados con etiquetas de texto pueden encontrarse más tarde con la búsqueda marked:"=el-texto-que-haya-introdudo".

Extraer metadata de los nombres de archivo

Normalmente, calibre lee los metadatos a partir del contenido del archivo del libro. Sin embargo, puede configurarse para leer los metadatos a partir del nombre de archivo, por medio de Preferencias > Importar y exportar > Añadir libros > Leer metadatos desde el contenido del archivo.

También puede controlar cómo se leen los metadatos desde el nombre del archivo utilizando expresiones regulares (ver Todo acerca de cómo utilizar expresiones regulares en calibre). En la sección Añadir libros del cuadro de diálogo de configuración, puede especificar una expresión regular que calibre utilizará para tratar de adivinar los metadatos a partir de los nombres de los archivos de libro electrónico que se añaden a la biblioteca. La expresión regular predeterminada es:

title - author

es decir, asume que todos los caracteres hasta el primer - son el título del libro y los caracteres subsiguientes son el autor del libro. Por ejemplo, el nombre de archivo:

Foundation and Earth - Isaac Asimov.txt

será interpretado con el título: «Foundation and Earth» y autor: «Isaac Asimov»

Truco

Si el nombre del archivo no contiene ningún guión, la expresión regular anterior fallará.

Detalles del libro

El panel de detalles del libro muestra la portada y los metadatos para el libro actualmente seleccionado. Estos datos pueden ocultarse pulsando en el botón que se encuentra en la esquina inferior derecha de la ventana principal de calibre. Se puede pulsar sobre los nombres de autor en el panel de detalles del libro, y al hacerlo el sistema abrirá el artículo de Wikipedia del respectivo autor. Esta opción se puede personalizar pulsando con el botón derecho sobre el nombre del autor y seleccionando la opción Administrar este autor.

De manera similar, si se descargan metadatos del libro, el panel de detalles del libro mostrará automáticamente enlaces a las páginas del libro en Amazon, WorldCat, etc. desde donde se descargaron los metadatos.

Puede pulsar con el botón derecho individualmente en los formatos de libro electrónico en el panel de detalles del libro para eliminarlos, compararlos con sus versiones originales, guardarlos en un disco, abrirlos con un programa externo, etc.

Puede cambiar la portada del libro arrastrando y soltando una imagen en el panel de detalles del libro. Si desea modificar la imagen de portada en un programa externo, pulse con el botón derecho sobre la imagen y elija Abrir portada con….

También puede añadir archivos de libro electrónico para el libro actual simplemente arrastrando y soltando los archivos en el panel de detalles del libro.

Al pulsar dos veces en el panel de detalles del libro la información se desplegará en una nueva ventana.

Finalmente, puede personalizar qué información se mostrará en el panel de detalles del libro en Preferencias > Interfaz > Apariencia > Detalles del libro.

Explorador de etiquetas

El explorador de etiquetas le permite navegar en la colección por autor, etiquetas, serie, etc. Si pulsa en cualquiera de los elementos del explorador de etiquetas, por ejemplo el nombre del autor Isaac Asimov, la lista de libros a la derecha mostrará solamente los libros de ese autor. Puede pulsar también en las diversas categorías. Por ejemplo, pulsando «Serie» obtendrá una lista de los libros en cualquier serie.

La primera pulsación sobre un elemento restringe la lista de libros a aquellos que contienen o coinciden con un elemento. Siguiendo con el ejemplo anterior, si pulsamos sobre «Isaac Asimov» se mostrarán los libros de este autor. Al pulsar otra vez sobre el elemento se cambia lo que se muestra, dependiendo de si el elemento tiene descendientes (ver subcategorías y elementos jerárquicos más adelante). En el mismo ejemplo, si pulsamos otra vez sobre «Isaac Asimov» se restringe la lista de libros a los que no son de Isaac Asimov. Una tercera pulsación elimina la restricción y muestra todos los libros. Si mantiene pulsada la tecla Ctrl o Mayús y pulsa en varios elementos, se crearán restricciones basadas en varios elementos. Por ejemplo, puede mantener pulsada la tecla Ctrl y pulsar en las etiquetas «Historia» y «Europa» para buscar libros sobre historia europea. El explorador de etiquetas construye expresiones de búsqueda que se introducen automáticamente en la barra de búsqueda. Examinar lo que genera el explorador de etiquetas es una buena manera de aprender cómo construir expresiones básicas.

Los iconos de los elementos del explorador de etiquetas están coloreados parcialmente. La cantidad de color depende de la calificación promedio de los libros en la categoría. Por ejemplo, si los libros de Isaac Asimov tienen una calificación media de cuatro estrellas, el icono para Isaac Asimov en el explorador de etiquetas está coloreado en 4/5. Puede colocar el cursor del ratón sobre el icono para ver la calificación promedio.

Los elementos más externos del Explorador de etiquetas, como Autores y Series, se llaman categorías. Puede crear nuevas categorías, llamadas Categorías de usuario, que resultan útiles para organizar los libros. Por ejemplo, puede usar el Editor de categorías de usuario (pulse el botón Configurar en la parte inferior izquierda del Explorador de etiquetas y elija Administrar autores, etiquetas, etc. > Categorías de usuario) para crear una categoría llamada Autores favoritos y luego asignar los elementos de sus autores favoritos a la categoría. Las categorías de usuario pueden tener subcategorías. Por ejemplo, la categoría de usuario Favoritos.Autores es una subcategoría de Favoritos. Podría tener también Favoritos.Series, y entonces habría dos subcategorías dentro de Favoritos. Puede crear subcategorías pulsando con el botón derecho sobre una categoría de usuario, eligiendo Añadir una subcategoría a … e introduciendo el nombre de la subcategoría; o usando el Editor de categorías de usuario e introduciendo nombres como en el ejemplo anterior de «Favoritos».

Puede buscar categorías de usuario de la misma manera que las categorías predefinidas, pulsando sobre ellas. Hay cuatro tipos de búsqueda por las que se atraviesa al pulsar:

«todo lo que coincida con un elemento de la categoría», indicado por un solo signo más verde.

«todo lo que coincida con un elemento de la categoría o sus subcategorías», indicado por dos signos más verdes.

«todo lo que no coincida con ningún elemento de la categoría», indicado por un signo menos rojo.

«todo lo que no coincida con ningún elemento de la categoría o sus subcategorías», indicado por dos signos menos rojos.

También es posible crear jerarquías dentro de algunas categorías de texto como las etiquetas, series y columnas de texto. Estas jerarquías se muestran como un pequeño triángulo, permitiendo ocultar los subelementos. Para usar jerarquías de elementos en una categoría debe ir a Preferencias > Interfaz > Apariencia e introducir el nombre de la(s) categoría(s) en el campo «Categorías con elementos jerárquicos». Una vez hecho esto, los elementos en dicha categoría que contengan puntos se mostrarán usando el pequeño triángulo. Por ejemplo, supongamos que crea una columna personalizada llamada «Género» e indica que contiene elementos jerárquicos. Los elementos tales como «Misterio.Intriga» y «Misterio.Español» se mostrarán como «Misterio» con el triángulo al lado. Al pulsar sobre el triángulo se mostrarán «Intriga» y «Español» como subelementos. Véase Gestionar subgrupos de libros, por ejemplo «género» para más información.

Los elementos jerárquicos (elementos que tienen descendientes) usan las mismas cuatro búsquedas al pulsar que las categorías de usuario. Los elementos sin descendientes usan dos de las búsquedas: «todo lo que coincida» y «todo lo que no coincida».

Puede arrastrar y soltar elementos del explorador de etiquetas sobre las categorías de usuario para añadirlos a la categoría. Si el origen es una categoría de usuario y se mantiene pulsada la tecla Mayús mientras se arrastra, se moverá el elemento a la nueva categoría. También puede arrastrar y soltar libros de la lista de libros sobre elementos en el explorador de etiquetas; al soltar un libro sobre un elemento, éste se aplicará sobre el libro soltado. Por ejemplo, si se arrastra un libro sobre «Isaac Asimov» se establecerá el autor de dicho libro en «Isaac Asimov». Si se arrastra sobre la etiqueta «Historia» se añadirá la etiqueta «Historia» a las etiquetas del libro.

Puede encontrar cualquier elemento en el explorador de etiquetas pulsando en el botón de búsqueda en la esquina inferior derecha. Además, puede pulsar con el botón derecho sobre cualquier elemento y elegir entre varias operaciones. Algunos ejemplos son: ocultarlo, cambiarle el nombre o abrir un cuadro de diálogo de «Administrar x», donde puede administrar los elementos de ese tipo. Por ejemplo el cuadro Administrar autores le permite cambiar los nombres de los autores y controlár cómo se ordenan.

Puede controlar cómo se ordenan los elementos del explorador de etiquetas por medio del botón Configurar en la parte inferior izquierda del explorador de etiquetas. Puede elegir entre ordenar por nombre, calificación promedio o popularidad (popularidad es el número de libros con un elemento en la biblioteca; por ejemplo, la popularidad de Isaac Asimov es el número de libros de Isaac Asimov en la biblioteca).

Cuadro de portadas

Puede hacer que calibre muestre un cuadro de portadas en lugar de una lista de libros, si prefiere ver la biblioteca como portadas en lugar de nombres. El Cuadro de portadas se activa pulsando en el botón Distribución en la esquina inferior derecha de la ventana principal de calibre. Puede personalizar los tamaños de las portadas y el fondo del Cuadro de portadas en Preferencias > Interfaz > Apariencia > Cuadro de portadas. Puede también mostrar cualquier campo especificado bajo las portadas, como el título, autores, calificación, o una columna personalizada de diseño propio.

Explorador de portadas

Además del Cuadro de portadas descrito anteriormente, puede hacer que calibre muestre las portadas en una sola fila. Esto se activa por medio del botón Distribución en la esquina inferior derecha de la ventana principal. En Preferencias > Interfaz > Apariencia > Explorador de portadas puede cambiar el número de portadas que se muestran o hacer que el Explorador de portadas se abra en una ventana aparte.

Añadir notas para autores, series, etc.

Puede añadir notas para un autor, serie, etiqueta, editorial, etc. en la biblioteca de calibre. Para ello pulse con el botón derecho sobre el nombre de autor en el Explorador de etiquetas a la izquierda o en el panel de Detalles del libro a la derecha y elija Crear nota or Modificar nota.

Una sencilla ventana emergente le permitirá introducir notas usando un formato básico que permite enlaces e imágenes. Una vez creada una nota para un autor, puede visualizarse desde el panel de Detalles del libro pulsando sobre el pequeño icono de lápiz junto al nombre de autor.

Puede buscar en todas las notas de la biblioteca usando la herramienta Explorar notas, cel atajo de teclado Ctrl+Mayús+N o añadiéndola a la barra de herramientas mediante Preferencias > Barras de herramientas y menús.

Vista rápida

En ocasiones querrá seleccionar un libro y obtener rápidamente una lista de libros con el mismo valor de alguna categoría (autores, etiquetas, editorial, serie, etc.) que el libro seleccionado, pero sin cambiar la vista actual de la biblioteca. Puede conseguirlo con la vista rápida. La vista rápida abre una segunda ventana o un panel en la lista de libros que muestra la lista de los libros que coinciden con el valor de interés. Por ejemplo, supongamos que quiere ver una lista de todos los libros con alguno de los autores del libro actualmente seleccionado. Pulse en la casilla de autor que le interesa y pulse la tecla «Q» o el icono de Vista rápida en la sección de :guilabel.`Distribución` de la ventana de calibre. Se abrirá una ventana o un panel con todos los autores del libro a la izquierda y todos los libros del autor seleccionado a la derecha.

Algunos ejemplos de uso de la vista rápida: ver rápidamente qué otros libros:

tienen alguna etiqueta que el libro actualmente seleccionado tiene aplicada,

están en la misma serie que el libro actual

tienen los mismos valores en una columna personalizada que el libro actual

están escrtos por alguno de los autores del libro actual

comparten valores de una columna personalizada

Hay dos posibles ubicaciones para la información de vista rápida:

Puede abrirse «desacoplada»: por encima de la ventana de calibre y se mantendrá abierta hasta que la cierre.

Puede abrirse «acoplada»: como un panel en la sección de lista de libros de la ventana principal de calibre.

Puede cambiar la ventana de acoplada a desacoplada según desee con el botón «Acoplar o desacoplar»

El panel de vista rápida puede dejarse abierto permanentemente, y entonces seguirá los movimientos de la lista de libros. Por ejemplo, si pulsa en la vista de biblioteca de calibre sobre una columna de categoría (etiquetas, serie, editorial, autores, etc.) para un libro, el contenido de la ventana de vista rápida cambiará para mostrar en el panel izquierdo los valores de dicha categoría para el libro seleccionado (por ejemplo, las etiquetas para el libro). El primer elemento de la lista será seleccionado y la vista rápida mostrará en el panel derecho todos los libros de la biblioteca que usan ese valor. Pulse en otro valor del panel izquierdo para ver los libros con ese otro valor.

Pulse dos veces sobre un libro en la ventana de vista rápida para seleccionar ese libro en la vista de biblioteca. Esto también cambiará los elementos que se muestran en la ventana de vista rápida (el panel izquierdo) para mostrar los elementos en el nuevo libro seleccionado.

Pulse dos veces manteniendo pulsada la tecla Mayús o Ctrl sobre un libro en la ventana de vista rápida para abrir el cuadro de modificación de metadatos para dicho libro en la ventana de calibre. El libro modificado estará seleccionado en la vista rápida cuando cierre el cuadro modificación de metadatos.

Puede ver si una columna puede ser objeto de la vista rápida colocando el cursor del ratón sobre el encabezado de la columna y mirando la ayuda emergente de dicho encabezado. También puede pulsar con el botón derecho sobre el encabezado de la columna para ver si la opción «Vista rápida» se muestra en el menú, en cuyo caso elegir la opción Vista rápida es equivalente a pulsar «Q» en la celda actual.

Opciones (en Preferencias > Apariencia > Vista rápida):

Respetar (o no) la biblioteca virtual actual. Si se activa, la vista rápida mostrará sólo los libros en la biblioteca virtual actual. De manera predeterminada se respetan las bibliotecas virtuales.

Cambiar los contenidos de la ventana de vista rápida cuando se cambia la columna en la lista de libros con las teclas de desplazamiento. De manera predeterminada no se siguen los cambios realizados con las teclas de desplazamiento

Cambiar la columna activa en la vista rápida cuando se pulsa dos veces sobre una casilla en la ventana de vista rápida. De lo contrario, se cambia el libro pero no la columna que se examina. De manera predeterminada se cambia la columna

Cambiar la columna activa en la vista rápida a la columna actual cuando se pulsa la tecla Intro en el panel de vista rápida. De lo contrario, se cambia el libro pero no la columna que se examina. De manera predeterminada se cambia la columna

Elegir qué columnas se muestran en la ventana o panel de vista rápida.

Tareas

El panel de tareas muestra el número de tareas actualmente en ejecución. Las tareas son procesos que se ejecutan de manera separada. Incluyen la conversión de libros y la comunicación con el dispositivo lector. Puede pulsar sobre el panel de tareas para acceder a la lista de tareas. Una vez completada una tarea puede ver un registro detallado de dicha tarea pulsando dos veces sobre ella en la lista. Esto es útil para depurar tareas que no se hayan completado con éxito.

Atajos de teclado

calibre tiene varios atajos de teclado que pueden ahorrarle tiempo y movimientos de ratón. Estos atajos están activos en la vista de lista de libros (mientras no se estén modificando los detalles de un libro particular), y la mayoría de ellos afecta al libro que esté seleccionado. El visor de libros de calibre tiene sus propios atajos que pueden personalizarse pulsando en el botón Preferencias del visor.

Nota

Nota: Los atajos de teclado de calibre no requieren una tecla modificadora (Alt, Opción, Control, etc.), a no ser que se especifique lo contrario. Sólo necesita presionar tecla correspondiente, por ejemplo E para editar.

Atajos de teclado para el programa principal de calibre

Atajos de teclado

Acción

F2 (Tecla Intro en macOS)

Modificar los metadatos del campo actualmente seleccionado en la lista de libros

A

Añadir libros

Mayús+A

Añadir formatos a los libros seleccionados

C

Convertir los libros seleccionados

D

Enviar al dispositivo

Supr

Eliminar los libros seleccionados

E

Modificar metadatos de los libros seleccionados

G

Obtener libros

I

Mostrar detalles del libro

K

Modificar índice

M

Unir los registros de libros seleccionados

Alt+M

Unir los registros seleccionados, mantener los originales

O

Abrir carpeta contenedora

P

Pulir libros

S

Guardar en el disco

T

Modificar libro

V

Mostrar

Mayús+V

Ver el último libro leído

Alt+V (Cmd+V en macOS)

Mostrar un formato específico

Alt+Mayús+J

Conmutar lista de tareas

Alt+Mayús+B

Conmutar explorador de portadas

Alt+Mayús+D

Conmutar detalles del libro

Alt+Mayús+T

Conmutar explorador de etiquetas

Alt+Mayús+G

Conmutar cuadro de portadas

Alt+A

Mostrar libros del mismo autor que el libro actual

Alt+T

Mostrar libros con las mismas etiquetas que el libro actual

Alt+P

Mostrar libros de la misma editorial que el libro actual

Alt+Mayús+S

Mostrar libros de la misma serie que el libro actual

/, Ctrl+F

Activar la barra de búsqueda

Ctrl+Mayús+F

Abrir el cuadro de diálogo de búsqueda avanzada

Mayús+Alt+F

Conmutar la barra de búsqueda

Esc

Limpiar la búsqueda actual

Mayús+Esc

Llevar el foco a la lista de libros

Ctrl+Esc

Limpiar la biblioteca virtual

Alt+Esc

Limpiar la restricción adicional

Ctrl+*

Crear una biblioteca virtual temporal basada en la búsqueda actual

Ctrl+Derecha

Seleccionar la siguiente pestaña de biblioteca virtual

Ctrl+Izquierda

Seleccionar la pestaña de biblioteca virtual anterior

N, F3

Encontrar el siguiente libro que coincida con el criterio de búsqueda (sólo funciona si el resaltado de búsquedas está activado en las preferencias de búsqueda)

Mayús+N, Mayús+F3

Encontrar el libro anterior que coincida con el criterio de búsqueda (sólo funciona si el resaltado de búsquedas está activado en las preferencias de búsqueda)

Ctrl+D

Descargar metadatos y portadas

Ctrl+R

Reiniciar calibre

Ctrl+Mayús+R

Reiniciar calibre en modo de depuración

Ctrl+Mayús+E

Añadir libros vacíos a calibre

Ctrl+M

Conmutar marca para los libros seleccionados

Ctrl+/ o Ctrl+Alt+F

Abrir la ventana emergente para buscar en el texto completo de todos los libros de la biblioteca

Q

Abrir la ventana de vista rápida para ver libros relacionados por serie, etiquetas, etc.

Mayús+Q

Llevar el foco al panel de vista rápida abierto

Mayús+S

Buscar en el panel de vista rápida

F5

Volver a aplicar el orden actual

Ctrl+Q

Salir de calibre

X

Conmutar el desplazamiento automático de la lista de libros

Ctrl+Alt+Mayús+F

Restringir los libros mostrados a sólo aquellos libros que están en una categoría mostrada actualmente en el Explorador de etiquetas

B

Explorar anotaciones (resaltados y marcadores) creados en el visor de calibre para todos los libros de la biblioteca

Ctrl+Mayús+N

Explorar las notas asociadas con autores, series, etiquetas, etc.

Alt+Mayús+L

Alternar el diseño entre vista ancha y estrecha

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.web.feeds.news

Código fuente para calibre.web.feeds.news

__license__   = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Defines various abstract base classes that can be subclassed to create powerful news fetching recipes.
'''
__docformat__ = "restructuredtext en"

import io
import os
import re
import sys
import time
import traceback
from collections import defaultdict
from contextlib import closing
from urllib.parse import urlparse, urlsplit

from calibre import __appname__, as_unicode, browser, force_unicode, iswindows, preferred_encoding, random_user_agent, strftime
from calibre.ebooks.BeautifulSoup import BeautifulSoup, CData, NavigableString, Tag
from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPFCreator
from calibre.ebooks.metadata.toc import TOC
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.date import now as nowf
from calibre.utils.icu import numeric_sort_key
from calibre.utils.img import add_borders_to_image, image_to_data, save_cover_data_to
from calibre.utils.localization import _, canonicalize_lang, ngettext
from calibre.utils.logging import ThreadSafeWrapper
from calibre.utils.threadpool import NoResultsPending, ThreadPool, WorkRequest
from calibre.web import Recipe
from calibre.web.feeds import Feed, feed_from_xml, feeds_from_index, templates
from calibre.web.fetch.simple import AbortArticle, RecursiveFetcher
from calibre.web.fetch.simple import option_parser as web2disk_option_parser
from calibre.web.fetch.utils import prepare_masthead_image
from polyglot.builtins import string_or_bytes

def classes(classes):
    q = frozenset(classes.split(' '))
    return dict(attrs={
        'class': lambda x: x and frozenset(x.split()).intersection(q)})

def prefixed_classes(classes):
    q = frozenset(classes.split(' '))

    def matcher(x):
        if x:
            for candidate in frozenset(x.split()):
                for x in q:
                    if candidate.startswith(x):
                        return True
        return False
    return {'attrs': {'class': matcher}}

class LoginFailed(ValueError):
    pass

class DownloadDenied(ValueError):
    pass

[documentos] class BasicNewsRecipe(Recipe): ''' Base class that contains logic needed in all recipes. By overriding progressively more of the functionality in this class, you can make progressively more customized/powerful recipes. For a tutorial introduction to creating recipes, see :doc:`news`. ''' #: The title to use for the e-book title = _('Unknown News Source') #: A couple of lines that describe the content this recipe downloads. #: This will be used primarily in a GUI that presents a list of recipes. description = '' #: The author of this recipe __author__ = __appname__ #: Minimum calibre version needed to use this recipe requires_version = (0, 6, 0) #: The language that the news is in. Must be an ISO-639 code either #: two or three characters long language = 'und' #: Maximum number of articles to download from each feed. This is primarily #: useful for feeds that don't have article dates. For most feeds, you should #: use :attr:`BasicNewsRecipe.oldest_article` max_articles_per_feed = 100 #: Oldest article to download from this news source. In days. oldest_article = 7.0 #: Number of levels of links to follow on article webpages recursions = 0 #: The default delay between consecutive downloads in seconds. The argument may be a #: floating point number to indicate a more precise time. See :meth:`get_url_specific_delay` #: to implement per URL delays. delay = 0 #: Publication type #: Set to newspaper, magazine or blog. If set to None, no publication type #: metadata will be written to the opf file. publication_type = 'unknown' #: Number of simultaneous downloads. Set to 1 if the server is picky. #: Automatically reduced to 1 if :attr:`BasicNewsRecipe.delay` > 0 simultaneous_downloads = 5 #: Timeout for fetching files from server in seconds timeout = 120.0 #: The format string for the date shown on the first page. #: By default: Day_Name, Day_Number Month_Name Year timefmt = ' [%a, %d %b %Y]' #: List of feeds to download. #: Can be either ``[url1, url2, ...]`` or ``[('title1', url1), ('title2', url2),...]`` feeds = None #: Max number of characters in the short description summary_length = 500 #: Convenient flag to disable loading of stylesheets for websites #: that have overly complex stylesheets unsuitable for conversion #: to e-book formats. #: If True stylesheets are not downloaded and processed no_stylesheets = False #: Convenient flag to strip all JavaScript tags from the downloaded HTML remove_javascript = True #: If True the GUI will ask the user for a username and password #: to use while downloading. #: If set to "optional" the use of a username and password becomes optional needs_subscription = False #: If True the navigation bar is center aligned, otherwise it is left aligned center_navbar = True #: Specify an override encoding for sites that have an incorrect #: charset specification. The most common being specifying ``latin1`` and #: using ``cp1252``. If None, try to detect the encoding. If it is a #: callable, the callable is called with two arguments: The recipe object #: and the source to be decoded. It must return the decoded source. encoding = None #: Normally we try to guess if a feed has full articles embedded in it #: based on the length of the embedded content. If `None`, then the #: default guessing is used. If `True` then the we always assume the feeds has #: embedded content and if `False` we always assume the feed does not have #: embedded content. use_embedded_content = None #: Set to True and implement :meth:`get_obfuscated_article` to handle #: websites that try to make it difficult to scrape content. articles_are_obfuscated = False #: Reverse the order of articles in each feed reverse_article_order = False #: Automatically extract all the text from downloaded article pages. Uses #: the algorithms from the readability project. Setting this to True, means #: that you do not have to worry about cleaning up the downloaded HTML #: manually (though manual cleanup will always be superior). auto_cleanup = False #: Specify elements that the auto cleanup algorithm should never remove. #: The syntax is a XPath expression. For example:: #: #: auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with #: id="article-image" #: auto_cleanup_keep = '//*[@class="important"]' will keep all elements #: with class="important" #: auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]' #: will keep all divs with id="article-image" and spans #: with class="important" #: auto_cleanup_keep = None #: Specify any extra :term:`CSS` that should be added to downloaded :term:`HTML` files. #: It will be inserted into `<style>` tags, just before the closing #: `</head>` tag thereby overriding all :term:`CSS` except that which is #: declared using the style attribute on individual :term:`HTML` tags. #: Note that if you want to programmatically generate the extra_css override #: the :meth:`get_extra_css()` method instead. #: For example:: #: #: extra_css = '.heading { font: serif x-large }' #: extra_css = None #: If True empty feeds are removed from the output. #: This option has no effect if parse_index is overridden in #: the sub class. It is meant only for recipes that return a list #: of feeds using `feeds` or :meth:`get_feeds`. It is also used if you use #: the ignore_duplicate_articles option. remove_empty_feeds = False #: List of regular expressions that determines which links to follow. #: If empty, it is ignored. Used only if is_link_wanted is #: not implemented. For example:: #: #: match_regexps = [r'page=[0-9]+'] #: #: will match all URLs that have `page=some number` in them. #: #: Only one of :attr:`BasicNewsRecipe.match_regexps` or #: :attr:`BasicNewsRecipe.filter_regexps` should be defined. match_regexps = [] #: List of regular expressions that determines which links to ignore. #: If empty it is ignored. Used only if is_link_wanted is not #: implemented. For example:: #: #: filter_regexps = [r'ads\.doubleclick\.net'] #: #: will remove all URLs that have `ads.doubleclick.net` in them. #: #: Only one of :attr:`BasicNewsRecipe.match_regexps` or #: :attr:`BasicNewsRecipe.filter_regexps` should be defined. filter_regexps = [] #: Recipe specific options to control the conversion of the downloaded #: content into an e-book. These will override any user or plugin specified #: values, so only use if absolutely necessary. For example:: #: #: conversion_options = { #: 'base_font_size' : 16, #: 'linearize_tables' : True, #: } #: conversion_options = {} #: List of tags to be removed. Specified tags are removed from downloaded HTML. #: A tag is specified as a dictionary of the form:: #: #: { #: name : 'tag name', #e.g. 'div' #: attrs : a dictionary, #e.g. {'class': 'advertisment'} #: } #: #: All keys are optional. For a full explanation of the search criteria, see #: `Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree>`__ #: A common example:: #: #: remove_tags = [dict(name='div', class_='advert')] #: #: This will remove all `<div class="advert">` tags and all #: their children from the downloaded :term:`HTML`. remove_tags = [] #: Remove all tags that occur after the specified tag. #: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`. #: For example:: #: #: remove_tags_after = [dict(id='content')] #: #: will remove all #: tags after the first element with `id="content"`. remove_tags_after = None #: Remove all tags that occur before the specified tag. #: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`. #: For example:: #: #: remove_tags_before = dict(id='content') #: #: will remove all #: tags before the first element with `id="content"`. remove_tags_before = None #: List of attributes to remove from all tags. #: For example:: #: #: remove_attributes = ['style', 'font'] remove_attributes = [] #: Keep only the specified tags and their children. #: For the format for specifying a tag see :attr:`BasicNewsRecipe.remove_tags`. #: If this list is not empty, then the `<body>` tag will be emptied and re-filled with #: the tags that match the entries in this list. For example:: #: #: keep_only_tags = [dict(id=['content', 'heading'])] #: #: will keep only tags that have an `id` attribute of `"content"` or `"heading"`. keep_only_tags = [] #: List of :term:`regexp` substitution rules to run on the downloaded :term:`HTML`. #: Each element of the #: list should be a two element tuple. The first element of the tuple should #: be a compiled regular expression and the second a callable that takes #: a single match object and returns a string to replace the match. For example:: #: #: preprocess_regexps = [ #: (re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE), #: lambda match: '</body>'), #: ] #: #: will remove everything from `<!--Article ends here-->` to `</body>`. preprocess_regexps = [] #: The CSS that is used to style the templates, i.e., the navigation bars and #: the Tables of Contents. Rather than overriding this variable, you should #: use `extra_css` in your recipe to customize look and feel. template_css = ''' .article_date { color: gray; font-family: monospace; } .article_description { text-indent: 0pt; } a.article { font-weight: bold; text-align:left; } a.feed { font-weight: bold; } .calibre_navbar { font-family:monospace; } ''' #: By default, calibre will use a default image for the masthead (Kindle only). #: Override this in your recipe to provide a URL to use as a masthead. masthead_url = None #: By default, the cover image returned by get_cover_url() will be used as #: the cover for the periodical. Overriding this in your recipe instructs #: calibre to render the downloaded cover into a frame whose width and height #: are expressed as a percentage of the downloaded cover. #: cover_margins = (10, 15, '#ffffff') pads the cover with a white margin #: 10px on the left and right, 15px on the top and bottom. #: Color names are defined `here <https://www.imagemagick.org/script/color.php>`_. #: Note that for some reason, white does not always work in Windows. Use #: #ffffff instead cover_margins = (0, 0, '#ffffff') #: Set to a non empty string to disable this recipe. #: The string will be used as the disabled message recipe_disabled = None #: Ignore duplicates of articles that are present in more than one section. #: A duplicate article is an article that has the same title and/or URL. #: To ignore articles with the same title, set this to:: #: #: ignore_duplicate_articles = {'title'} #: #: To use URLs instead, set it to:: #: #: ignore_duplicate_articles = {'url'} #: #: To match on title or URL, set it to:: #: #: ignore_duplicate_articles = {'title', 'url'} ignore_duplicate_articles = None # The following parameters control how the recipe attempts to minimize # JPEG image sizes #: Set this to False to ignore all scaling and compression parameters and #: pass images through unmodified. If True and the other compression #: parameters are left at their default values, JPEG images will be scaled to fit #: in the screen dimensions set by the output profile and compressed to size at #: most (w * h)/16 where w x h are the scaled image dimensions. compress_news_images = False #: The factor used when auto compressing JPEG images. If set to None, #: auto compression is disabled. Otherwise, the images will be reduced in size to #: (w * h)/compress_news_images_auto_size bytes if possible by reducing #: the quality level, where w x h are the image dimensions in pixels. #: The minimum JPEG quality will be 5/100 so it is possible this constraint #: will not be met. This parameter can be overridden by the parameter #: compress_news_images_max_size which provides a fixed maximum size for images. #: Note that if you enable scale_news_images_to_device then the image will #: first be scaled and then its quality lowered until its size is less than #: (w * h)/factor where w and h are now the *scaled* image dimensions. In #: other words, this compression happens after scaling. compress_news_images_auto_size = 16 #: Set JPEG quality so images do not exceed the size given (in KBytes). #: If set, this parameter overrides auto compression via compress_news_images_auto_size. #: The minimum JPEG quality will be 5/100 so it is possible this constraint #: will not be met. compress_news_images_max_size = None #: Rescale images to fit in the device screen dimensions set by the output profile. #: Ignored if no output profile is set. scale_news_images_to_device = True #: Maximum dimensions (w,h) to scale images to. If scale_news_images_to_device is True #: this is set to the device screen dimensions set by the output profile unless #: there is no profile set, in which case it is left at whatever value it has been #: assigned (default None). scale_news_images = None #: If set to True then links in downloaded articles that point to other downloaded articles are #: changed to point to the downloaded copy of the article rather than its original web URL. If you #: set this to True, you might also need to implement :meth:`canonicalize_internal_url` to work #: with the URL scheme of your particular website. resolve_internal_links = False #: Set to False if you do not want to use gzipped transfers. Note that some old servers flake out with gzip handle_gzip = True # See the built-in recipes for examples of these settings. def short_title(self): return force_unicode(self.title, preferred_encoding)

[documentos] def is_link_wanted(self, url, tag): ''' Return True if the link should be followed or False otherwise. By default, raises NotImplementedError which causes the downloader to ignore it. :param url: The URL to be followed :param tag: The tag from which the URL was derived ''' raise NotImplementedError()

[documentos] def get_extra_css(self): ''' By default returns `self.extra_css`. Override if you want to programmatically generate the extra_css. ''' return self.extra_css

[documentos] def get_cover_url(self): ''' Return a :term:`URL` to the cover image for this issue or `None`. By default it returns the value of the member `self.cover_url` which is normally `None`. If you want your recipe to download a cover for the e-book override this method in your subclass, or set the member variable `self.cover_url` before this method is called. ''' return getattr(self, 'cover_url', None)

[documentos] def get_masthead_url(self): ''' Return a :term:`URL` to the masthead image for this issue or `None`. By default it returns the value of the member `self.masthead_url` which is normally `None`. If you want your recipe to download a masthead for the e-book override this method in your subclass, or set the member variable `self.masthead_url` before this method is called. Masthead images are used in Kindle MOBI files. ''' return getattr(self, 'masthead_url', None)

[documentos] def get_feeds(self): ''' Return a list of :term:`RSS` feeds to fetch for this profile. Each element of the list must be a 2-element tuple of the form (title, url). If title is None or an empty string, the title from the feed is used. This method is useful if your recipe needs to do some processing to figure out the list of feeds to download. If so, override in your subclass. ''' if not self.feeds: raise NotImplementedError() if self.test: return self.feeds[:self.test[0]] return self.feeds

[documentos] def get_url_specific_delay(self, url): ''' Return the delay in seconds before downloading this URL. If you want to programmatically determine the delay for the specified URL, override this method in your subclass, returning self.delay by default for URLs you do not want to affect. :return: A floating point number, the delay in seconds. ''' return self.delay

[documentos] @classmethod def print_version(cls, url): ''' Take a `url` pointing to the webpage with article content and return the :term:`URL` pointing to the print version of the article. By default does nothing. For example:: def print_version(self, url): return url + '?&pagewanted=print' ''' raise NotImplementedError()

[documentos] @classmethod def image_url_processor(cls, baseurl, url): ''' Perform some processing on image urls (perhaps removing size restrictions for dynamically generated images, etc.) and return the precessed URL. Return None or an empty string to skip fetching the image. ''' return url

[documentos] def preprocess_image(self, img_data, image_url): ''' Perform some processing on downloaded image data. This is called on the raw data before any resizing is done. Must return the processed raw data. Return None to skip the image. ''' return img_data

[documentos] def get_browser(self, *args, **kwargs): ''' Return a browser instance used to fetch documents from the web. By default it returns a `mechanize <https://mechanize.readthedocs.io/en/latest/>`_ browser instance that supports cookies, ignores robots.txt, handles refreshes and has a mozilla firefox user agent. If your recipe requires that you login first, override this method in your subclass. For example, the following code is used in the New York Times recipe to login for full access:: def get_browser(self): br = BasicNewsRecipe.get_browser(self) if self.username is not None and self.password is not None: br.open('https://www.nytimes.com/auth/login') br.select_form(name='login') br['USERID'] = self.username br['PASSWORD'] = self.password br.submit() return br ''' if 'user_agent' not in kwargs: # More and more news sites are serving JPEG XR images to IE ua = getattr(self, 'last_used_user_agent', None) or self.calibre_most_common_ua or random_user_agent(allow_ie=False) kwargs['user_agent'] = self.last_used_user_agent = ua self.log('Using user agent:', kwargs['user_agent']) br = browser(*args, **kwargs) br.addheaders += [('Accept', '*/*')] if self.handle_gzip: br.set_handle_gzip(True) return br

[documentos] def clone_browser(self, br): ''' Clone the browser br. Cloned browsers are used for multi-threaded downloads, since mechanize is not thread safe. The default cloning routines should capture most browser customization, but if you do something exotic in your recipe, you should override this method in your recipe and clone manually. Cloned browser instances use the same, thread-safe CookieJar by default, unless you have customized cookie handling. ''' if callable(getattr(br, 'clone_browser', None)): return br.clone_browser() # Uh-oh recipe using something exotic, call get_browser return self.get_browser()

@property def cloned_browser(self): if hasattr(self.get_browser, 'is_base_class_implementation'): # We are using the default get_browser, which means no need to # clone br = BasicNewsRecipe.get_browser(self) else: br = self.clone_browser(self.browser) return br

[documentos] def get_article_url(self, article): ''' Override in a subclass to customize extraction of the :term:`URL` that points to the content for each article. Return the article URL. It is called with `article`, an object representing a parsed article from a feed. See `feedparser <https://pythonhosted.org/feedparser/>`_. By default it looks for the original link (for feeds syndicated via a service like FeedBurner or Pheedo) and if found, returns that or else returns `article.link <https://pythonhosted.org/feedparser/reference-entry-link.html>`_. ''' for key in article.keys(): if key.endswith('_origlink'): url = article[key] if url and (url.startswith('http://') or url.startswith('https://')): return url ans = article.get('link', None) if not ans and getattr(article, 'links', None): for item in article.links: if item.get('rel', 'alternate') == 'alternate': ans = item['href'] break return ans

[documentos] def skip_ad_pages(self, soup): ''' This method is called with the source of each downloaded :term:`HTML` file, before any of the cleanup attributes like remove_tags, keep_only_tags are applied. Note that preprocess_regexps will have already been applied. It is meant to allow the recipe to skip ad pages. If the soup represents an ad page, return the HTML of the real page. Otherwise return None. `soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`. ''' return None

[documentos] def abort_article(self, msg=None): ''' Call this method inside any of the preprocess methods to abort the download for the current article. Useful to skip articles that contain inappropriate content, such as pure video articles. ''' raise AbortArticle(msg or _('Article download aborted'))

[documentos] def preprocess_raw_html(self, raw_html, url): ''' This method is called with the source of each downloaded :term:`HTML` file, before it is parsed into an object tree. raw_html is a unicode string representing the raw HTML downloaded from the web. url is the URL from which the HTML was downloaded. Note that this method acts *before* preprocess_regexps. This method must return the processed raw_html as a unicode object. ''' return raw_html

def preprocess_raw_html_(self, raw_html, url): raw_html = self.preprocess_raw_html(raw_html, url) if self.auto_cleanup: try: raw_html = self.extract_readable_article(raw_html, url) except: self.log.exception('Auto cleanup of URL: %r failed'%url) return raw_html

[documentos] def preprocess_html(self, soup): ''' This method is called with the source of each downloaded :term:`HTML` file, before it is parsed for links and images. It is called after the cleanup as specified by remove_tags etc. It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`. It should return `soup` after processing it. `soup`: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`. ''' return soup

[documentos] def postprocess_html(self, soup, first_fetch): ''' This method is called with the source of each downloaded :term:`HTML` file, after it is parsed for links and images. It can be used to do arbitrarily powerful post-processing on the :term:`HTML`. It should return `soup` after processing it. :param soup: A `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`__ instance containing the downloaded :term:`HTML`. :param first_fetch: True if this is the first page of an article. ''' return soup

[documentos] def cleanup(self): ''' Called after all articles have been download. Use it to do any cleanup like logging out of subscription sites, etc. ''' pass

[documentos] def canonicalize_internal_url(self, url, is_link=True): ''' Return a set of canonical representations of ``url``. The default implementation uses just the server hostname and path of the URL, ignoring any query parameters, fragments, etc. The canonical representations must be unique across all URLs for this news source. If they are not, then internal links may be resolved incorrectly. :param is_link: Is True if the URL is coming from an internal link in an HTML file. False if the URL is the URL used to download an article. ''' try: parts = urlparse(url) except Exception: self.log.error('Failed to parse url: %r, ignoring' % url) return frozenset() nl = parts.netloc path = parts.path or '' if isinstance(nl, bytes): nl = nl.decode('utf-8', 'replace') if isinstance(path, bytes): path = path.decode('utf-8', 'replace') return frozenset({(nl, path.rstrip('/'))})

[documentos] def index_to_soup(self, url_or_raw, raw=False, as_tree=False, save_raw=None): ''' Convenience method that takes an URL to the index page and returns a `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc>`__ of it. `url_or_raw`: Either a URL or the downloaded index page as a string ''' if re.match((br'\w+://' if isinstance(url_or_raw, bytes) else r'\w+://'), url_or_raw): # We may be called in a thread (in the skip_ad_pages method), so # clone the browser to be safe. We cannot use self.cloned_browser # as it may or may not actually clone the browser, depending on if # the recipe implements get_browser() or not br = self.clone_browser(self.browser) open_func = getattr(br, 'open_novisit', br.open) with closing(open_func(url_or_raw, timeout=self.timeout)) as f: _raw = f.read() if not _raw: raise RuntimeError('Could not fetch index from %s'%url_or_raw) else: _raw = url_or_raw if raw: return _raw if not isinstance(_raw, str) and self.encoding: if callable(self.encoding): _raw = self.encoding(_raw) else: _raw = _raw.decode(self.encoding, 'replace') from calibre.ebooks.chardet import strip_encoding_declarations, xml_to_unicode from calibre.utils.cleantext import clean_xml_chars if isinstance(_raw, str): _raw = strip_encoding_declarations(_raw) else: _raw = xml_to_unicode(_raw, strip_encoding_pats=True, resolve_entities=True)[0] _raw = clean_xml_chars(_raw) if save_raw: with open(save_raw, 'wb') as f: f.write(_raw.encode('utf-8')) if as_tree: from html5_parser import parse return parse(_raw) return BeautifulSoup(_raw)

[documentos] def extract_readable_article(self, html, url): ''' Extracts main article content from 'html', cleans up and returns as a (article_html, extracted_title) tuple. Based on the original readability algorithm by Arc90. ''' from lxml.html import document_fromstring, fragment_fromstring, tostring from calibre.ebooks.readability import readability doc = readability.Document(html, self.log, url=url, keep_elements=self.auto_cleanup_keep) article_html = doc.summary() extracted_title = doc.title() try: frag = fragment_fromstring(article_html) except: doc = document_fromstring(article_html) frag = doc.xpath('//body')[-1] if frag.tag == 'html': root = frag elif frag.tag == 'body': root = document_fromstring( '<html><head><title>%s</title></head></html>' % extracted_title) root.append(frag) else: root = document_fromstring( '<html><head><title>%s</title></head><body/></html>' % extracted_title) root.xpath('//body')[0].append(frag) body = root.xpath('//body')[0] has_title = False for x in body.iterdescendants(): if x.text == extracted_title: has_title = True inline_titles = body.xpath('//h1|//h2') if not has_title and not inline_titles: heading = body.makeelement('h2') heading.text = extracted_title body.insert(0, heading) raw_html = tostring(root, encoding='unicode') return raw_html

[documentos] def sort_index_by(self, index, weights): ''' Convenience method to sort the titles in `index` according to `weights`. `index` is sorted in place. Returns `index`. `index`: A list of titles. `weights`: A dictionary that maps weights to titles. If any titles in index are not in weights, they are assumed to have a weight of 0. ''' weights = defaultdict(int, weights) index.sort(key=lambda x: weights[x]) return index

[documentos] def parse_index(self): ''' This method should be implemented in recipes that parse a website instead of feeds to generate a list of articles. Typical uses are for news sources that have a "Print Edition" webpage that lists all the articles in the current print edition. If this function is implemented, it will be used in preference to :meth:`BasicNewsRecipe.parse_feeds`. It must return a list. Each element of the list must be a 2-element tuple of the form ``('feed title', list of articles)``. Each list of articles must contain dictionaries of the form:: { 'title' : article title, 'url' : URL of print version, 'date' : The publication date of the article as a string, 'description' : A summary of the article 'content' : The full article (can be an empty string). Obsolete do not use, instead save the content to a temporary file and pass a file:///path/to/temp/file.html as the URL. } For an example, see the recipe for downloading `The Atlantic`. In addition, you can add 'author' for the author of the article. If you want to abort processing for some reason and have calibre show the user a simple message instead of an error, call :meth:`abort_recipe_processing`. ''' raise NotImplementedError()

[documentos] def abort_recipe_processing(self, msg): ''' Causes the recipe download system to abort the download of this recipe, displaying a simple feedback message to the user. ''' from calibre.ebooks.conversion import ConversionUserFeedBack raise ConversionUserFeedBack(_('Failed to download %s')%self.title, msg)

[documentos] def get_obfuscated_article(self, url): ''' If you set `articles_are_obfuscated` this method is called with every article URL. It should return the path to a file on the filesystem that contains the article HTML. That file is processed by the recursive HTML fetching engine, so it can contain links to pages/images on the web. Alternately, you can return a dictionary of the form: {'data': <HTML data>, 'url': <the resolved URL of the article>}. This avoids needing to create temporary files. The `url` key in the dictionary is useful if the effective URL of the article is different from the URL passed into this method, for example, because of redirects. It can be omitted if the URL is unchanged. This method is typically useful for sites that try to make it difficult to access article content automatically. ''' raise NotImplementedError()

[documentos] def add_toc_thumbnail(self, article, src): ''' Call this from populate_article_metadata with the src attribute of an <img> tag from the article that is appropriate for use as the thumbnail representing the article in the Table of Contents. Whether the thumbnail is actually used is device dependent (currently only used by the Kindles). Note that the referenced image must be one that was successfully downloaded, otherwise it will be ignored. ''' if not src or not hasattr(article, 'toc_thumbnail'): return src = src.replace('\\', '/') if re.search(r'feed_\d+/article_\d+/images/img', src, flags=re.I) is None: self.log.warn('Ignoring invalid TOC thumbnail image: %r'%src) return article.toc_thumbnail = re.sub(r'^.*?feed', 'feed', src, flags=re.IGNORECASE)

[documentos] def populate_article_metadata(self, article, soup, first): ''' Called when each HTML page belonging to article is downloaded. Intended to be used to get article metadata like author/summary/etc. from the parsed HTML (soup). :param article: A object of class :class:`calibre.web.feeds.Article`. If you change the summary, remember to also change the text_summary :param soup: Parsed HTML belonging to this article :param first: True iff the parsed HTML is the first page of the article. ''' pass

[documentos] def postprocess_book(self, oeb, opts, log): ''' Run any needed post processing on the parsed downloaded e-book. :param oeb: An OEBBook object :param opts: Conversion options ''' pass

def __init__(self, options, log, progress_reporter): ''' Initialize the recipe. :param options: Parsed commandline options :param log: Logging object :param progress_reporter: A Callable that takes two arguments: progress (a number between 0 and 1) and a string message. The message should be optional. ''' self.log = ThreadSafeWrapper(log) if not isinstance(self.title, str): self.title = str(self.title, 'utf-8', 'replace') self.debug = options.verbose > 1 self.output_dir = os.path.abspath(os.getcwd()) self.verbose = options.verbose self.test = options.test if self.test and not isinstance(self.test, tuple): self.test = (2, 2) self.username = options.username self.password = options.password self.lrf = options.lrf self.output_profile = options.output_profile self.touchscreen = getattr(self.output_profile, 'touchscreen', False) if self.touchscreen: self.template_css += self.output_profile.touchscreen_news_css if self.test: self.max_articles_per_feed = self.test[1] self.simultaneous_downloads = min(4, self.simultaneous_downloads) if self.debug: self.verbose = True self.report_progress = progress_reporter if self.needs_subscription and ( self.username is None or self.password is None or ( not self.username and not self.password)): if self.needs_subscription != 'optional': raise ValueError(_('The "%s" recipe needs a username and password.')%self.title) self.browser = self.get_browser() self.image_map, self.image_counter = {}, 1 self.css_map = {} web2disk_cmdline = ['web2disk', '--timeout', str(self.timeout), '--max-recursions', str(self.recursions), '--delay', str(self.delay), ] if self.verbose: web2disk_cmdline.append('--verbose') if self.no_stylesheets: web2disk_cmdline.append('--dont-download-stylesheets') for reg in self.match_regexps: web2disk_cmdline.extend(['--match-regexp', reg]) for reg in self.filter_regexps: web2disk_cmdline.extend(['--filter-regexp', reg]) if options.output_profile.short_name in ('default', 'tablet'): self.scale_news_images_to_device = False elif self.scale_news_images_to_device: self.scale_news_images = options.output_profile.screen_size self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0] for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps', 'skip_ad_pages', 'preprocess_html', 'remove_tags_after', 'remove_tags_before', 'is_link_wanted', 'compress_news_images', 'compress_news_images_max_size', 'compress_news_images_auto_size', 'scale_news_images'): setattr(self.web2disk_options, extra, getattr(self, extra)) self.web2disk_options.postprocess_html = self._postprocess_html self.web2disk_options.preprocess_image = self.preprocess_image self.web2disk_options.encoding = self.encoding self.web2disk_options.preprocess_raw_html = self.preprocess_raw_html_ self.web2disk_options.get_delay = self.get_url_specific_delay if self.delay > 0: self.simultaneous_downloads = 1 self.navbar = templates.TouchscreenNavBarTemplate() if self.touchscreen else \ templates.NavBarTemplate() self.failed_downloads = [] self.partial_failures = [] self.aborted_articles = [] def _postprocess_html(self, soup, first_fetch, job_info): if self.no_stylesheets: for link in soup.findAll('link'): if (link.get('type') or 'text/css').lower() == 'text/css' and 'stylesheet' in (link.get('rel') or ('stylesheet',)): link.extract() for style in soup.findAll('style'): style.extract() head = soup.find('head') if not head: head = soup.find('body') if not head: head = soup.find(True) css = self.template_css + '\n\n' + (self.get_extra_css() or '') style = soup.new_tag('style', type='text/css', title='override_css') style.append(css) head.append(style) if first_fetch and job_info: url, f, a, feed_len = job_info body = soup.find('body') if body is not None: templ = self.navbar.generate(False, f, a, feed_len, not self.has_single_feed, url, __appname__, center=self.center_navbar, extra_css=self.get_extra_css() or '') elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div') body.insert(0, elem) # This is needed because otherwise inserting elements into # the soup breaks find() soup = BeautifulSoup(soup.decode_contents()) if self.remove_javascript: for script in list(soup.findAll('script')): script.extract() for o in soup.findAll(onload=True): del o['onload'] for attr in self.remove_attributes: for x in soup.findAll(attrs={attr:True}): del x[attr] for bad_tag in list(soup.findAll(['base', 'iframe', 'canvas', 'embed', 'button', 'command', 'datalist', 'video', 'audio', 'noscript', 'link', 'meta'])): # link tags can be used for preloading causing network activity in # calibre viewer. meta tags can do all sorts of crazy things, # including http-equiv refresh, viewport shenanigans, etc. bad_tag.extract() # srcset causes some viewers, like calibre's to load images from the # web, and it also possible causes iBooks on iOS to barf, see # https://bugs.launchpad.net/bugs/1713986 for img in soup.findAll('img', srcset=True): del img['srcset'] ans = self.postprocess_html(soup, first_fetch) # Nuke HTML5 tags for x in ans.findAll(['article', 'aside', 'header', 'footer', 'nav', 'figcaption', 'figure', 'section']): x.name = 'div' if job_info: url, f, a, feed_len = job_info try: article = self.feed_objects[f].articles[a] except: self.log.exception('Failed to get article object for postprocessing') pass else: self.populate_article_metadata(article, ans, first_fetch) return ans

[documentos] def download(self): ''' Download and pre-process all articles from the feeds in this recipe. This method should be called only once on a particular Recipe instance. Calling it more than once will lead to undefined behavior. :return: Path to index.html ''' try: res = self.build_index() self.report_progress(1, _('Download finished')) if self.failed_downloads: self.log.warning(_('Failed to download the following articles:')) for feed, article, debug in self.failed_downloads: self.log.warning(article.title, 'from', feed.title) self.log.debug(article.url) self.log.debug(debug) if self.partial_failures: self.log.warning(_('Failed to download parts of the following articles:')) for feed, atitle, aurl, debug in self.partial_failures: self.log.warning(atitle + _(' from ') + feed) self.log.debug(aurl) self.log.warning(_('\tFailed links:')) for l, tb in debug: self.log.warning(l) self.log.debug(tb) return res finally: self.cleanup()

@property def lang_for_html(self): try: lang = self.language.replace('_', '-').partition('-')[0].lower() if lang == 'und': lang = None except: lang = None return lang def feeds2index(self, feeds): templ = (templates.TouchscreenIndexTemplate if self.touchscreen else templates.IndexTemplate) templ = templ(lang=self.lang_for_html) css = self.template_css + '\n\n' +(self.get_extra_css() or '') timefmt = self.timefmt return templ.generate(self.title, "mastheadImage.jpg", timefmt, feeds, extra_css=css).render(doctype='xhtml') @classmethod def description_limiter(cls, src): if not src: return '' src = force_unicode(src, 'utf-8') pos = cls.summary_length fuzz = 50 si = src.find(';', pos) if si > 0 and si-pos > fuzz: si = -1 gi = src.find('>', pos) if gi > 0 and gi-pos > fuzz: gi = -1 npos = max(si, gi) if npos < 0: npos = pos ans = src[:npos+1] if len(ans) < len(src): from calibre.utils.cleantext import clean_xml_chars # Truncating the string could cause a dangling UTF-16 half-surrogate, which will cause lxml to barf, clean it ans = clean_xml_chars(ans) + '\u2026' return ans def feed2index(self, f, feeds): feed = feeds[f] if feed.image_url is not None: # Download feed image imgdir = os.path.join(self.output_dir, 'images') if not os.path.isdir(imgdir): os.makedirs(imgdir) if feed.image_url in self.image_map: feed.image_url = self.image_map[feed.image_url] else: bn = urlsplit(feed.image_url).path if bn: bn = bn.rpartition('/')[-1] if bn: img = os.path.join(imgdir, 'feed_image_%d%s'%(self.image_counter, os.path.splitext(bn)[-1])) try: with open(img, 'wb') as fi, closing(self.browser.open(feed.image_url, timeout=self.timeout)) as r: fi.write(r.read()) self.image_counter += 1 feed.image_url = img self.image_map[feed.image_url] = img except: pass if isinstance(feed.image_url, bytes): feed.image_url = feed.image_url.decode(sys.getfilesystemencoding(), 'strict') templ = (templates.TouchscreenFeedTemplate if self.touchscreen else templates.FeedTemplate) templ = templ(lang=self.lang_for_html) css = self.template_css + '\n\n' +(self.get_extra_css() or '') return templ.generate(f, feeds, self.description_limiter, extra_css=css).render(doctype='xhtml') def _fetch_article(self, url, dir_, f, a, num_of_feeds, preloaded=None): br = self.browser if hasattr(self.get_browser, 'is_base_class_implementation'): # We are using the default get_browser, which means no need to # clone br = BasicNewsRecipe.get_browser(self) else: br = self.clone_browser(self.browser) self.web2disk_options.browser = br fetcher = RecursiveFetcher(self.web2disk_options, self.log, self.image_map, self.css_map, (url, f, a, num_of_feeds)) fetcher.browser = br fetcher.base_dir = dir_ fetcher.current_dir = dir_ fetcher.show_progress = False fetcher.image_url_processor = self.image_url_processor if preloaded is not None: fetcher.preloaded_urls[url] = preloaded res, path, failures = fetcher.start_fetch(url), fetcher.downloaded_paths, fetcher.failed_links if not res or not os.path.exists(res): msg = _('Could not fetch article.') + ' ' if self.debug: msg += _('The debug traceback is available earlier in this log') else: msg += _('Run with -vv to see the reason') raise Exception(msg) return res, path, failures def fetch_article(self, url, dir, f, a, num_of_feeds): return self._fetch_article(url, dir, f, a, num_of_feeds) def fetch_obfuscated_article(self, url, dir, f, a, num_of_feeds): x = self.get_obfuscated_article(url) if isinstance(x, dict): data = x['data'] if isinstance(data, str): data = data.encode(self.encoding or 'utf-8') url = x.get('url', url) else: with open(x, 'rb') as of: data = of.read() os.remove(x) return self._fetch_article(url, dir, f, a, num_of_feeds, preloaded=data) def fetch_embedded_article(self, article, dir, f, a, num_of_feeds): templ = templates.EmbeddedContent() raw = templ.generate(article).render('html') with PersistentTemporaryFile('_feeds2disk.html') as pt: pt.write(raw) url = ('file:'+pt.name) if iswindows else ('file://'+pt.name) return self._fetch_article(url, dir, f, a, num_of_feeds) def remove_duplicate_articles(self, feeds): seen_keys = defaultdict(set) remove = [] for f in feeds: for article in f: for key in self.ignore_duplicate_articles: val = getattr(article, key) seen = seen_keys[key] if val: if val in seen: remove.append((f, article)) else: seen.add(val) for feed, article in remove: self.log.debug('Removing duplicate article: %s from section: %s'%( article.title, feed.title)) feed.remove_article(article) if self.remove_empty_feeds: feeds = [f for f in feeds if len(f) > 0] return feeds def build_index(self): self.report_progress(0, _('Fetching feeds...')) feeds = None try: feeds = feeds_from_index(self.parse_index(), oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, log=self.log) self.report_progress(0, _('Got feeds from index page')) except NotImplementedError: pass if feeds is None: feeds = self.parse_feeds() if not feeds: raise ValueError('No articles found, aborting') if self.ignore_duplicate_articles is not None: feeds = self.remove_duplicate_articles(feeds) self.report_progress(0, _('Trying to download cover...')) self.download_cover() self.report_progress(0, _('Generating masthead...')) self.resolve_masthead() if self.test: feeds = feeds[:self.test[0]] self.has_single_feed = len(feeds) == 1 index = os.path.join(self.output_dir, 'index.html') html = self.feeds2index(feeds) with open(index, 'wb') as fi: fi.write(html) self.jobs = [] if self.reverse_article_order: for feed in feeds: if hasattr(feed, 'reverse'): feed.reverse() self.feed_objects = feeds for f, feed in enumerate(feeds): feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) if not os.path.isdir(feed_dir): os.makedirs(feed_dir) for a, article in enumerate(feed): if a >= self.max_articles_per_feed: break art_dir = os.path.join(feed_dir, 'article_%d'%a) if not os.path.isdir(art_dir): os.makedirs(art_dir) try: url = self.print_version(article.url) except NotImplementedError: url = article.url except: self.log.exception('Failed to find print version for: '+article.url) url = None if not url: continue func, arg = (self.fetch_embedded_article, article) \ if self.use_embedded_content or (self.use_embedded_content is None and feed.has_embedded_content()) \ else \ ((self.fetch_obfuscated_article if self.articles_are_obfuscated else self.fetch_article), url) req = WorkRequest(func, (arg, art_dir, f, a, len(feed)), {}, (f, a), self.article_downloaded, self.error_in_article_download) req.feed = feed req.article = article req.feed_dir = feed_dir self.jobs.append(req) self.jobs_done = 0 tp = ThreadPool(self.simultaneous_downloads) for req in self.jobs: tp.putRequest(req, block=True, timeout=0) self.report_progress(0, ngettext( 'Starting download in a single thread...', 'Starting download [{} threads]...', self.simultaneous_downloads).format(self.simultaneous_downloads)) while True: try: tp.poll() time.sleep(0.1) except NoResultsPending: break for f, feed in enumerate(feeds): html = self.feed2index(f,feeds) feed_dir = os.path.join(self.output_dir, 'feed_%d'%f) with open(os.path.join(feed_dir, 'index.html'), 'wb') as fi: fi.write(html) self.create_opf(feeds) self.report_progress(1, _('Feeds downloaded to %s')%index) return index def _download_cover(self): self.cover_path = None try: cu = self.get_cover_url() except Exception as err: self.log.error(_('Could not download cover: %s')%as_unicode(err)) self.log.debug(traceback.format_exc()) else: if not cu: return cdata = None if hasattr(cu, 'read'): cdata = cu.read() cu = getattr(cu, 'name', 'cover.jpg') elif os.access(cu, os.R_OK): with open(cu, 'rb') as f: cdata = f.read() else: self.report_progress(1, _('Downloading cover from %s')%cu) with closing(self.browser.open(cu, timeout=self.timeout)) as r: cdata = r.read() if not cdata: return ext = cu.split('/')[-1].rpartition('.')[-1].lower().strip() if ext == 'pdf': from calibre.ebooks.metadata.pdf import get_metadata stream = io.BytesIO(cdata) cdata = None mi = get_metadata(stream) if mi.cover_data and mi.cover_data[1]: cdata = mi.cover_data[1] if not cdata: return if self.cover_margins[0] or self.cover_margins[1]: cdata = image_to_data(add_borders_to_image(cdata, left=self.cover_margins[0],right=self.cover_margins[0], top=self.cover_margins[1],bottom=self.cover_margins[1], border_color=self.cover_margins[2])) cpath = os.path.join(self.output_dir, 'cover.jpg') save_cover_data_to(cdata, cpath) self.cover_path = cpath def download_cover(self): self.cover_path = None try: self._download_cover() except: self.log.exception('Failed to download cover') self.cover_path = None def _download_masthead(self, mu): if hasattr(mu, 'rpartition'): ext = mu.rpartition('.')[-1] if '?' in ext: ext = '' else: ext = mu.name.rpartition('.')[-1] ext = ext.lower() if ext else 'jpg' mpath = os.path.join(self.output_dir, 'masthead_source.'+ext) outfile = os.path.join(self.output_dir, 'mastheadImage.jpg') if hasattr(mu, 'read'): with open(mpath, 'wb') as mfile: mfile.write(mu.read()) elif os.access(mu, os.R_OK): with open(mpath, 'wb') as mfile: mfile.write(open(mu, 'rb').read()) else: with open(mpath, 'wb') as mfile, closing(self.browser.open(mu, timeout=self.timeout)) as r: mfile.write(r.read()) self.report_progress(1, _('Masthead image downloaded')) self.prepare_masthead_image(mpath, outfile) self.masthead_path = outfile if os.path.exists(mpath): os.remove(mpath) def download_masthead(self, url): try: self._download_masthead(url) except: self.log.exception("Failed to download supplied masthead_url") def resolve_masthead(self): self.masthead_path = None try: murl = self.get_masthead_url() except: self.log.exception('Failed to get masthead url') murl = None if murl is not None: # Try downloading the user-supplied masthead_url # Failure sets self.masthead_path to None self.download_masthead(murl) if self.masthead_path is None: self.log.info("Synthesizing mastheadImage") self.masthead_path = os.path.join(self.output_dir, 'mastheadImage.jpg') try: self.default_masthead_image(self.masthead_path) except: self.log.exception('Failed to generate default masthead image') self.masthead_path = None

[documentos] def default_cover(self, cover_file): ''' Create a generic cover for recipes that don't have a cover ''' try: from calibre.ebooks.covers import create_cover title = self.title if isinstance(self.title, str) else \ self.title.decode(preferred_encoding, 'replace') date = strftime(self.timefmt).replace('[', '').replace(']', '') img_data = create_cover(title, [date]) cover_file.write(img_data) cover_file.flush() except: self.log.exception('Failed to generate default cover') return False return True

[documentos] def get_masthead_title(self): 'Override in subclass to use something other than the recipe title' return self.title

MI_WIDTH = 600 MI_HEIGHT = 60 def default_masthead_image(self, out_path): from calibre.ebooks import generate_masthead generate_masthead(self.get_masthead_title(), output_path=out_path, width=self.MI_WIDTH, height=self.MI_HEIGHT) def prepare_masthead_image(self, path_to_image, out_path): prepare_masthead_image(path_to_image, out_path, self.MI_WIDTH, self.MI_HEIGHT)

[documentos] def publication_date(self): ''' Use this method to set the date when this issue was published. Defaults to the moment of download. Must return a :class:`datetime.datetime` object. ''' return nowf()

def create_opf(self, feeds, dir=None): if dir is None: dir = self.output_dir title = self.short_title() pdate = self.publication_date() if self.output_profile.periodical_date_in_title: title += strftime(self.timefmt, pdate) mi = MetaInformation(title, [__appname__]) mi.publisher = __appname__ mi.author_sort = __appname__ if self.publication_type: mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title() mi.timestamp = nowf() article_titles, aseen = [], set() for (af, aa) in self.aborted_articles: aseen.add(aa.title) for (ff, fa, tb) in self.failed_downloads: aseen.add(fa.title) for f in feeds: for a in f: if a.title and a.title not in aseen: aseen.add(a.title) article_titles.append(force_unicode(a.title, 'utf-8')) desc = self.description if not isinstance(desc, str): desc = desc.decode('utf-8', 'replace') mi.comments = (_('Articles in this issue:' ) + '\n\n' + '\n\n'.join(article_titles)) + '\n\n' + desc language = canonicalize_lang(self.language) if language is not None: mi.language = language mi.pubdate = pdate opf_path = os.path.join(dir, 'index.opf') ncx_path = os.path.join(dir, 'index.ncx') opf = OPFCreator(dir, mi) # Add mastheadImage entry to <guide> section mp = getattr(self, 'masthead_path', None) if mp is not None and os.access(mp, os.R_OK): from calibre.ebooks.metadata.opf2 import Guide ref = Guide.Reference(os.path.basename(self.masthead_path), os.getcwd()) ref.type = 'masthead' ref.title = 'Masthead Image' opf.guide.append(ref) manifest = [os.path.join(dir, 'feed_%d'%i) for i in range(len(feeds))] manifest.append(os.path.join(dir, 'index.html')) manifest.append(os.path.join(dir, 'index.ncx')) # Get cover cpath = getattr(self, 'cover_path', None) if cpath is None: pf = open(os.path.join(dir, 'cover.jpg'), 'wb') if self.default_cover(pf): cpath = pf.name if cpath is not None and os.access(cpath, os.R_OK): opf.cover = cpath manifest.append(cpath) # Get masthead mpath = getattr(self, 'masthead_path', None) if mpath is not None and os.access(mpath, os.R_OK): manifest.append(mpath) opf.create_manifest_from_files_in(manifest) for mani in opf.manifest: if mani.path.endswith('.ncx'): mani.id = 'ncx' if mani.path.endswith('mastheadImage.jpg'): mani.id = 'masthead-image' entries = ['index.html'] toc = TOC(base_path=dir) self.play_order_counter = 0 self.play_order_map = {} self.article_url_map = aumap = defaultdict(set) def feed_index(num, parent): f = feeds[num] for j, a in enumerate(f): if getattr(a, 'downloaded', False): adir = 'feed_%d/article_%d/'%(num, j) auth = a.author if not auth: auth = None desc = a.text_summary if not desc: desc = None else: desc = self.description_limiter(desc) tt = a.toc_thumbnail if a.toc_thumbnail else None entries.append('%sindex.html'%adir) po = self.play_order_map.get(entries[-1], None) if po is None: self.play_order_counter += 1 po = self.play_order_counter arelpath = '%sindex.html'%adir for curl in self.canonicalize_internal_url(a.orig_url, is_link=False): aumap[curl].add(arelpath) article_toc_entry = parent.add_item(arelpath, None, a.title if a.title else _('Untitled article'), play_order=po, author=auth, description=desc, toc_thumbnail=tt) for entry in a.internal_toc_entries: anchor = entry.get('anchor') if anchor: self.play_order_counter += 1 po += 1 article_toc_entry.add_item( arelpath, entry['anchor'], entry['title'] or _('Unknown section'), play_order=po ) last = os.path.join(self.output_dir, ('%sindex.html'%adir).replace('/', os.sep)) for sp in a.sub_pages: prefix = os.path.commonprefix([opf_path, sp]) relp = sp[len(prefix):] entries.append(relp.replace(os.sep, '/')) last = sp if os.path.exists(last): with open(last, 'rb') as fi: src = fi.read().decode('utf-8') soup = BeautifulSoup(src) body = soup.find('body') if body is not None: prefix = '/'.join('..'for i in range(2*len(re.findall(r'link\d+', last)))) templ = self.navbar.generate(True, num, j, len(f), not self.has_single_feed, a.orig_url, __appname__, prefix=prefix, center=self.center_navbar) elem = BeautifulSoup(templ.render(doctype='xhtml').decode('utf-8')).find('div') body.insert(len(body.contents), elem) with open(last, 'wb') as fi: fi.write(str(soup).encode('utf-8')) if len(feeds) == 0: raise Exception('All feeds are empty, aborting.') if len(feeds) > 1: for i, f in enumerate(feeds): entries.append('feed_%d/index.html'%i) po = self.play_order_map.get(entries[-1], None) if po is None: self.play_order_counter += 1 po = self.play_order_counter auth = getattr(f, 'author', None) if not auth: auth = None desc = getattr(f, 'description', None) if not desc: desc = None feed_index(i, toc.add_item('feed_%d/index.html'%i, None, f.title, play_order=po, description=desc, author=auth)) else: entries.append('feed_%d/index.html'%0) feed_index(0, toc) for i, p in enumerate(entries): entries[i] = os.path.join(dir, p.replace('/', os.sep)) opf.create_spine(entries) opf.set_toc(toc) with open(opf_path, 'wb') as opf_file, open(ncx_path, 'wb') as ncx_file: opf.render(opf_file, ncx_file) def article_downloaded(self, request, result): index = os.path.join(os.path.dirname(result[0]), 'index.html') if index != result[0]: if os.path.exists(index): os.remove(index) os.rename(result[0], index) a = request.requestID[1] article = request.article self.log.debug('Downloaded article:', article.title, 'from', article.url) article.orig_url = article.url article.url = 'article_%d/index.html'%a article.downloaded = True article.sub_pages = result[1][1:] self.jobs_done += 1 self.report_progress(float(self.jobs_done)/len(self.jobs), _('Article downloaded: %s')%force_unicode(article.title)) if result[2]: self.partial_failures.append((request.feed.title, article.title, article.url, result[2])) def error_in_article_download(self, request, traceback): self.jobs_done += 1 if traceback and re.search('^AbortArticle:', traceback, flags=re.M) is not None: self.log.warn('Aborted download of article:', request.article.title, 'from', request.article.url) self.report_progress(float(self.jobs_done)/len(self.jobs), _('Article download aborted: %s')%force_unicode(request.article.title)) self.aborted_articles.append((request.feed, request.article)) else: self.log.error('Failed to download article:', request.article.title, 'from', request.article.url) self.log.debug(traceback) self.log.debug('\n') self.report_progress(float(self.jobs_done)/len(self.jobs), _('Article download failed: %s')%force_unicode(request.article.title)) self.failed_downloads.append((request.feed, request.article, traceback))

[documentos] def parse_feeds(self): ''' Create a list of articles from the list of feeds returned by :meth:`BasicNewsRecipe.get_feeds`. Return a list of :class:`Feed` objects. ''' feeds = self.get_feeds() parsed_feeds = [] br = self.browser for obj in feeds: if isinstance(obj, string_or_bytes): title, url = None, obj else: title, url = obj if isinstance(title, bytes): title = title.decode('utf-8') if isinstance(url, bytes): url = url.decode('utf-8') if url.startswith('feed://'): url = 'http'+url[4:] self.report_progress(0, _('Fetching feed')+' %s...'%(title if title else url)) try: purl = urlparse(url, allow_fragments=False) if purl.username or purl.password: hostname = purl.hostname if purl.port: hostname += f':{purl.port}' url = purl._replace(netloc=hostname).geturl() if purl.username and purl.password: br.add_password(url, purl.username, purl.password) with closing(br.open_novisit(url, timeout=self.timeout)) as f: raw = f.read() parsed_feeds.append(feed_from_xml( raw, title=title, log=self.log, oldest_article=self.oldest_article, max_articles_per_feed=self.max_articles_per_feed, get_article_url=self.get_article_url )) except Exception as err: feed = Feed() msg = 'Failed feed: %s'%(title if title else url) feed.populate_from_preparsed_feed(msg, []) feed.description = as_unicode(err) parsed_feeds.append(feed) self.log.exception(msg) delay = self.get_url_specific_delay(url) if delay > 0: time.sleep(delay) remove = [fl for fl in parsed_feeds if len(fl) == 0 and self.remove_empty_feeds] for f in remove: parsed_feeds.remove(f) return parsed_feeds

[documentos] @classmethod def tag_to_string(self, tag, use_alt=True, normalize_whitespace=True): ''' Convenience method to take a `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_ :code:`Tag` and extract the text from it recursively, including any CDATA sections and alt tag attributes. Return a possibly empty Unicode string. `use_alt`: If `True` try to use the alt attribute for tags that don't have any textual content `tag`: `BeautifulSoup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/>`_ :code:`Tag` ''' if tag is None: return '' if isinstance(tag, string_or_bytes): return tag if callable(getattr(tag, 'xpath', None)) and not hasattr(tag, 'contents'): # a lxml tag from lxml.etree import tostring ans = tostring(tag, method='text', encoding='unicode', with_tail=False) else: strings = [] for item in tag.contents: if isinstance(item, (NavigableString, CData)): strings.append(item.string) elif isinstance(item, Tag): res = self.tag_to_string(item) if res: strings.append(res) elif use_alt: try: strings.append(item['alt']) except KeyError: pass ans = ''.join(strings) if normalize_whitespace: ans = re.sub(r'\s+', ' ', ans) return ans

@classmethod def soup(cls, raw): return BeautifulSoup(raw)

[documentos] @classmethod def adeify_images(cls, soup): ''' If your recipe when converted to EPUB has problems with images when viewed in Adobe Digital Editions, call this method from within :meth:`postprocess_html`. ''' for item in soup.findAll('img'): for attrib in ['height','width','border','align','style']: try: del item[attrib] except KeyError: pass oldParent = item.parent myIndex = oldParent.contents.index(item) item.extract() divtag = soup.new_tag('div') brtag = soup.new_tag('br') oldParent.insert(myIndex,divtag) divtag.append(item) divtag.append(brtag) return soup

def internal_postprocess_book(self, oeb, opts, log): if self.resolve_internal_links and self.article_url_map: seen = set() for item in oeb.spine: for a in item.data.xpath('//*[local-name()="a" and @href]'): if a.get('rel') == 'calibre-downloaded-from': continue url = a.get('href') for curl in self.canonicalize_internal_url(url): articles = self.article_url_map.get(curl) if articles: arelpath = sorted(articles, key=numeric_sort_key)[0] a.set('href', item.relhref(arelpath)) if url not in seen: log.debug(f'Resolved internal URL: {url} -> {arelpath}') seen.add(url)

class CustomIndexRecipe(BasicNewsRecipe):

    def custom_index(self):
        '''
        Return the filesystem path to a custom HTML document that will serve as the index for
        this recipe. The index document will typically contain many `<a href="...">`
        tags that point to resources on the internet that should be downloaded.
        '''
        raise NotImplementedError

    def create_opf(self):
        mi = MetaInformation(self.title + strftime(self.timefmt), [__appname__])
        mi.publisher = __appname__
        mi.author_sort = __appname__
        mi = OPFCreator(self.output_dir, mi)
        mi.create_manifest_from_files_in([self.output_dir])
        mi.create_spine([os.path.join(self.output_dir, 'index.html')])
        with open(os.path.join(self.output_dir, 'index.opf'), 'wb') as opf_file:
            mi.render(opf_file)

    def download(self):
        index = os.path.abspath(self.custom_index())
        url = 'file:'+index if iswindows else 'file://'+index
        self.web2disk_options.browser = self.clone_browser(self.browser)
        fetcher = RecursiveFetcher(self.web2disk_options, self.log)
        fetcher.base_dir = self.output_dir
        fetcher.current_dir = self.output_dir
        fetcher.show_progress = False
        res = fetcher.start_fetch(url)
        self.create_opf()
        return res

class AutomaticNewsRecipe(BasicNewsRecipe):

    auto_cleanup = True

class CalibrePeriodical(BasicNewsRecipe):

    #: Set this to the slug for the calibre periodical
    calibre_periodicals_slug = None

    LOG_IN = 'https://news.calibre-ebook.com/accounts/login'
    needs_subscription = True
    __author__ = 'calibre Periodicals'

    def get_browser(self):
        br = BasicNewsRecipe.get_browser(self)
        br.open(self.LOG_IN)
        br.select_form(name='login')
        br['username'] = self.username
        br['password'] = self.password
        raw = br.submit().read()
        if 'href="/my-account"' not in raw:
            raise LoginFailed(
                    _('Failed to log in, check your username and password for'
                    ' the calibre Periodicals service.'))

        return br
    get_browser.is_base_class_implementation = True

    def download(self):
        self.log('Fetching downloaded recipe')
        try:
            raw = self.browser.open_novisit(
                'https://news.calibre-ebook.com/subscribed_files/%s/0/temp.downloaded_recipe'
                % self.calibre_periodicals_slug
                    ).read()
        except Exception as e:
            if hasattr(e, 'getcode') and e.getcode() == 403:
                raise DownloadDenied(
                        _('You do not have permission to download this issue.'
                        ' Either your subscription has expired or you have'
                        ' exceeded the maximum allowed downloads for today.'))
            raise
        f = io.BytesIO(raw)
        from calibre.utils.zipfile import ZipFile
        zf = ZipFile(f)
        zf.extractall()
        zf.close()
        from glob import glob

        from calibre.web.feeds.recipes import compile_recipe
        try:
            recipe = compile_recipe(open(glob('*.recipe')[0],
                'rb').read())
            self.conversion_options = recipe.conversion_options
        except:
            self.log.exception('Failed to compile downloaded recipe')
        return os.path.abspath('index.html')

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Añadir su sitio de noticias favorito »

Documentación de la API para fórmulas

Documentación de la API para fórmulas

La API para crear fórmulas está definida por la clase BasicNewsRecipe

class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[fuente]

La clase base que contiene la lógica necesaria para todas las fórmulas. Reemplazando progresivamente las distintas funciones de esta clase, puede generar fórmulas cada vez más personalizadas y potentes. Para un cursillo introductorio a la creación de fórmulas, véase Añadir su sitio de noticias favorito.

abort_article(msg=None)[fuente]

Ejecute este método dentro de cualquiera de los métodos de preprocesado para cancelar la descarga del artículo actual. Es útil para evitar la descarga de artículos con contenido no adecuado, como artículos que sólo contienen vídeos.

abort_recipe_processing(msg)[fuente]

Hace que el sistema de descarga de fórmulas cancele la descarga de esta fórmula, mostrando al usuario un mensaje de información sencillo.

add_toc_thumbnail(article, src)[fuente]

Utilice este método desde populate_article_metadata con el atributo src de una etiqueta <img> del artículo que quiera usar como la miniatura que representa el artículo en el índice. Si la miniatura se usa realmente o no dependerá del dispositivo (actualmente sólo la usan los Kindles). Tenga en cuenta que la imagen a que se hace referencia debe haber sido descargada con éxito, de lo contrario se ignorará.

classmethod adeify_images(soup)[fuente]

Si la fórmula, al ser convertida a EPUB, tiene problemas con las imágenes cuando se muestra en Adobe Digital Editions, utilice este método desde postprocess_html().

canonicalize_internal_url(url, is_link=True)[fuente]

Devuelve un conjunto de representaciones canónicas de url. La implementación predeterminada usa el nombre del servidor y la ruta de acceso del URL, descartando parámetros de consulta, fragmentos, etc. Las representaciones canónicas deben ser únicas para todos los URL de esta fuente de noticias. Si no lo son, los enlaces internos pueden ser incorrectos.

Parámetros:

is_link – Es True si el URL viene de un enlace interno en un archivo HTML. False si el URL es el URL usado para descargar un artículo.

cleanup()[fuente]

Usado cuando todos los artículos han sido descargados. Úselo para hacer limpieza, como cerrar sesiones en los sitios que requieren suscripción, etc.

clone_browser(br)[fuente]

Clonar el navegador br. Los navegadores clonados se utilizan para las descargas multihilo, ya que mechanize no es seguro. Las rutinas de clonación predeterminadas deberían reconocer la mayoría de las personalizaciones del navegador, pero si se hace algo exótico en una fórmula, debe sustituir este método en la fórmula y clonarlo de forma manual.

Los navegadores clonados usan el mismo CookieJar seguro en varios hilos, de manera predeterminada, a menos que haya personalizado el manejo de las cookies.

default_cover(cover_file)[fuente]

Crear una portada genérica para las fórmulas que no poseen una.

download()[fuente]

Descargar y preprocesar todos los artículos de los canales RSS en esta fórmula. Este método debe utilizarse una sola vez en cada fórmula. Si se usa más de una vez se obtendrá un comportamiento indefinido. :return:`Ruta de acceso al index.html`

extract_readable_article(html, url)[fuente]

Extrae el contenido del artículo principal desde «html», lo depura y lo devuelve como una tupla (html_artículo, título_extraído). Basado en el algoritmo Readability original de Arc90.

get_article_url(article)[fuente]

Sustituir en una subclase para personalizar la extracción del URL que apunta al contenido de cada artículo. Devuelve el URL del artículo. Se ejecuta con article, un objeto que representa un artículo analizado de un canal. Véase feedparser [https://pythonhosted.org/feedparser/]. De manera predeterminada busca el enlace original (para canales agregados mediante un servicio como FeedBurner o Pheedo) y, si se encuentra, devuelve dicho enlace o article.link [https://pythonhosted.org/feedparser/reference-entry-link.html] en caso contrario.

get_browser(*args, **kwargs)[fuente]

Devuelve un navegador utilizado para obtener documentos desde Internet. De manera predeterminada devuelve un navegador mechanize [https://mechanize.readthedocs.io/en/latest/] que soporta cookies, ignora robots.txt, tiene en cuenta los refrescos y tiene un agente de usuario de tipo mozilla firefox.

Si la fórmula requiere un inicio de sesión, reemplace este método en la subclase. Por ejemplo, el siguiente código se usa en la fórmula del New York Times para iniciar la sesión y tener acceso total:

def get_browser(self):
    br = BasicNewsRecipe.get_browser(self)
    if self.username is not None and self.password is not None:
        br.open('https://www.nytimes.com/auth/login')
        br.select_form(name='login')
        br['USERID']   = self.username
        br['PASSWORD'] = self.password
        br.submit()
    return br

get_cover_url()[fuente]

Devuelve un URL para la imagen de portada de este número o None. De manera predeterminada, devuelve el valor del miembro self.cover_url que es normalmente None. Si quiere que la fórmula descargue una portada para el libro electrónico reemplace este método en la subclase, o establezca la variable miembro self.cover_url antes de utilizar este método.

get_extra_css()[fuente]

De manera predeterminada devuelve self.extra_css. Reemplácelo si desea generar el archivo extra_css mediante un programa.

get_feeds()[fuente]

Devuelve una lista de canales RSS para obtener en este perfil. Cada elemento de la lista debe ser una tupla de 2 elementos de la forma (título, url). Si el título es None o un texto vacío, se utiliza el título del canal RSS. Este método es útil si la fórmula tiene que hacer algún tipo de procesado para obtener la lista de canales RSS para descargar. Si es así, reemplácelo en la subclase.

get_masthead_title()[fuente]

Reemplácelo en la subclase para usar algo distinto del título de la fórmula

get_masthead_url()[fuente]

Devuelve un URL a la imagen de cabecera de este número o None. De manera predeterminada, devuelve el valor del miembro self.masthead_url que es normalmente None. Si quiere que la fórmula descargue una imagen de cabecera para el libro electrónico reemplace este método en la subclase, o establezca la variable miembro self.masthead_url antes de utilizar este método. Las imágenes de cabecera se usan en los archivos MOBI de Kindle.

get_obfuscated_article(url)[fuente]

Si activa articles_are_obfuscated, este método se ejecuta para cada URL de artítulo. Debe devolver la ruta a un archivo en el sistema de archivos que contiene el HTML del artículo. El archivo se procesa con el motor recursivo de HTML, por lo que puede contener enlaces a páginas o imágenes en la red. Alternativamente, puede devolver un diccionario de la forma {“data”: <datos HTML>, “url”: <el URL resuelto del artículo>}. Esto evita la creación de archivos temporales. La entrada url del diccionario es útil si el URL efectivo del artículo es distinto del URL que se le pasa a este método, por ejemplo debido a redirecciones. Se puede omitir si el URL no cambia.

Este método es normalmente útil para sitios que tratan de dificultar el acceso automático al contenido de los artículos.

get_url_specific_delay(url)[fuente]

Devuelve el retraso en segundos antes de descargar este URL. Si desea determinar mediante programación el retraso para el URL especificado, sustituya este método en la subclase y haga que devuelva self.delay de forma predeterminada para los URL que no desee afectar.

Devuelve:

Un número de coma flotante, el retraso en segundos.

classmethod image_url_processor(baseurl, url)[fuente]

Realiza algún procesado sobre las direcciones URL de las imágenes (tal vez eliminando restricciones de tamaño para imágenes generadas dinámicamente, etc.) y devuelve la dirección URL procesada. Devolver None o un texto vacío para omitir la imagen.

index_to_soup(url_or_raw, raw=False, as_tree=False, save_raw=None)[fuente]

Método práctico que toma el URL de una página de índice y devuelve un objeto BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc] que lo representa.

url_or_raw: Un URL o la página de índice descargada como texto

is_link_wanted(url, tag)[fuente]

Devuelve True si el enlace debe seguirse o False en caso contrario. De manera predeterminada, presenta NotImplementedError, que hace que el programa de descarga lo ignore.

Parámetros:

url – La dirección URL que debe seguirse

tag – La etiqueta de la que se extrajo la dirección URL

parse_feeds()[fuente]

Crea una lista de artículos desde la lista de canales devuelta por BasicNewsRecipe.get_feeds(). Devuelve una lista objetos Feed.

parse_index()[fuente]

Este método debe implementarse en las fórmulas que analizan una página de Internet en lugar de canales RSS para generar una lista de artículos. Normalmente se usa con fuentes de noticias que tienen una página del tipo «edición impresa» que enumera todos los artículos de la edición impresa actual. Si se implementa esta función, se usará con prioridad sobre BasicNewsRecipe.parse_feeds().

Debe devolver una lista. Cada elemento de la lista debe ser una tupla de 2 elementos de la forma ('título del canal', lista de artículos).

Cada lista de artículos deben contener diccionarios del tipo:

{
'title'       : article title,
'url'         : URL of print version,
'date'        : The publication date of the article as a string,
'description' : A summary of the article
'content'     : The full article (can be an empty string). Obsolete
                do not use, instead save the content to a temporary
                file and pass a file:///path/to/temp/file.html as
                the URL.
}

Como ejemplo, véase la fórmula para descargar The Atlantic. Además, puede añadir «author» para el autor del artículo.

Si desea cancelar el proceso por alguna razón y hacer que calibre muestre al usuario un simple mensaje en lugar de un error, use abort_recipe_processing().

populate_article_metadata(article, soup, first)[fuente]

Se ejecuta al descargar cada página HTML perteneciente al artículo article. Pensado para obtener metadatatos del artículo como autor, resumen, etc. del HTML procesado (soup).

Parámetros:

article – Un objeto de la clase calibre.web.feeds.Article. Si cambia el resumen, recuerde cambiar también text_summary

soup – HTML procesado perteneciente a este artículo

first – True si y sólo si el HTML procesado es la primera página del artículo.

postprocess_book(oeb, opts, log)[fuente]

Ejecutar cualquier procesado posterior necesario sobre el libro electrónico descargado.

Parámetros:

oeb – Un objeto OEBBook

opts – Opciones de conversión

postprocess_html(soup, first_fetch)[fuente]

Este método se ejecuta con el código fuente de cada archivo HTML, después de analizar sus imágenes y enlaces. Puede usarse para efectuar un procesado arbitrario sobre el HTML. Debe devolver soup después del procesado.

Parámetros:

soup – Un objeto BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] que contiene el HTML descargado.

first_fetch – True si ésta es la primera página de un artículo.

preprocess_html(soup)[fuente]

Este método se ejecuta con el código fuente de cada archivo HTML, antes de analizar sus imágenes y enlaces. Se ejecuta después de la limpieza especificada por remove_tags, etc. Puede usarse para efectuar un preprocesado arbitrario sobre el HTML. Debe devolver soup después del procesado.

soup: Un objeto BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] que contiene el HTML descargado.

preprocess_image(img_data, image_url)[fuente]

Procesa los datos de imagen descargados. Se ejecuta sobre los datos en bruto antes de cualquier redimensionado. Debe devolver los datos procesados. Para descartar la imagen devuelva None.

preprocess_raw_html(raw_html, url)[fuente]

Este método se ejecuta con el código fuente de cada archivo HTML, antes de convertirlo en un árbol de objetos. raw_html es un texto unicode que representa el HTML en bruto descargado de Internet. url es la dirección URL desde donde se descargó el HTML.

Tenga en cuenta que este método actúa antes de preprocess_regexps.

Este método debe devolver el archivo raw_html procesado como un objeto unicode.

classmethod print_version(url)[fuente]

Tomar un url que apunta a la página de Internet con el contenido del artículo y devuelve el URL de la versión para imprimir del artículo. De manera predeterminada no hace nada. Por ejemplo:

def print_version(self, url):
    return url + '?&pagewanted=print'

publication_date()[fuente]

Use este método para establecer la fecha de publicación de este número. De manera predeterminada es el momento de la descarga. Debe devolver un objeto datetime.datetime.

skip_ad_pages(soup)[fuente]

Este método se ejecuta con el código fuente de cada archivo HTML descargado, antes de aplicar ningún atributo de limpieza como remove_tags o keep_only_tags. Tenga en cuenta que preprocess_regexps ya se habrá aplicado. Está pensado para permitir que la fórmula evite las páginas de publicidad. Si soup representa una página de publicidad, devuelve el HTML de la página real. De lo contrario devuelve None.

soup: Un objeto BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] que contiene el HTML descargado.

sort_index_by(index, weights)[fuente]

Método práctico para ordenar los títulos en index según weights. index se ordena en su lugar. Devuelve index.

index: Una lista de títulos.

weights: Un diccionario que asigna pesos a los títulos. Si un título del índice no está en weights, se asume que tiene un peso de 0.

classmethod tag_to_string(tag, use_alt=True, normalize_whitespace=True)[fuente]

Método práctico que toma un objeto Tag de BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] y extrae el texto recursivamente, incluyendo secciones CDATA y atributos «alt». Devuelve un texto Unicode posiblemente vacío.

use_alt: Si es True intenta usar el atributo «alt» para las etiquetas que no poseen contenido textual

tag: Un objeto Tag de BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/]

articles_are_obfuscated = False

Establézcalo a True e implemente get_obfuscated_article() para manejar sitios de Internet que tratan de dificultar la obtención de contenido.

auto_cleanup = False

Extrae automáticamente todo el texto de las páginas de artículos descargadas. Utiliza los algoritmos del proyecto Readability. Si se establece en True, no hay necesidad de preocuparse de limpiar el código HTML manualmente (aunque la limpieza manual siempre será mejor).

auto_cleanup_keep = None

Especifica los elementos que el algoritmo de limpieza automática no debe eliminar nunca. La sintaxis es una expresión XPath. Por ejemplo:

auto_cleanup_keep = '//div[@id="article-image"]' will keep all divs with
                                               id="article-image"
auto_cleanup_keep = '//*[@class="important"]' will keep all elements
                                            with class="important"
auto_cleanup_keep = '//div[@id="article-image"]|//span[@class="important"]'
                  will keep all divs with id="article-image" and spans
                  with class="important"

center_navbar = True

Si es True la barra de navegación se alinea al centro, en caso contrario se alinea a la izquierda

compress_news_images = False

Si es False, se descartan todos los parámetros de escala y compresión y se dejan las imágenes sin modificar. Si es True y los otros parámetros de compresión se dejan en sus valores predeterminados, las imágenes JPEG se redimensionarán para adaptarse al tamaño de la pantalla establecido por el perfil de salida y se comprimen a un tamaño máximo de (w × h)/16, donde «w» y «h» son la anchura y altura redimensionadas de la imagen.

compress_news_images_auto_size = 16

El factor utilizado cuando se comprimen automáticamente las imágenes JPEG. Si se establece en None, la compresión automática se desactiva. De lo contrario, las imágenes se reducirán a un tamaño en bytes de (w × h)/compress_news_images_auto_size, si es posible, reduciendo el nivel de calidad, donde «w» y «h» son la anchura y la altura de la imagen en píxeles. La calidad mínima del JPEG será 5/100 por lo que es posible que no pueda cumplirse con esta restricción. Este parámetro se puede anular con el parámetro compress_news_images_max_size, que proporciona un tamaño máximo fijo para las imágenes. Tenga en cuenta que si se habilita scale_news_images_to_device entonces la imagen se ampliará primero y luego su calidad se reducirá hasta que su tamaño sea menor que (w × h)/factor donde «w» y «h» son ahora las dimensiones de la imagen redimensionada. En otras palabras, esta compresión ocurre después del redimensionado.

compress_news_images_max_size = None

Establece la calidad de los archivos JPEG para que las imágenes no excedan el tamaño dado (en kbytes). Si se establece este parámetro, la compresión automática vía compress_news_images_auto_size no tiene lugar. La calidad mínima del JPEG será 5/100 por lo que es posible que no pueda cumplirse con esta restricción.

conversion_options = {}

Opciones específicas de la fórmula para controlar la conversión del contenido descargado en un libro electrónico. Éstas tienen prioridad sobre cualquier valor especificado por el usuario o un complemento, por lo que deben utilizarse sólo si es absolutamente necesario. Por ejemplo:

conversion_options = {
  'base_font_size'   : 16,
  'linearize_tables' : True,
}

cover_margins = (0, 0, '#ffffff')

De manera predeterminada, la imagen de portada devuelta por get_cover_url() se utilizará como portada para la publicación. Si se establece un valor para esta variable en una fórmula, calibre colocará la portada descargada dentro de un marco cuya anchura y altura se expresan como un porcentaje de la portada descargada. cover_margins = (10, 15, “#ffffff”) rellena la portada con un margen blanco de 10px a la izquierda y derecha, 15px en la parte superior e inferior. Los nombres de los colores se definen aquí [https://www.imagemagick.org/script/color.php]. Tenga en cuenta que, por alguna razón, «white» no siempre funciona en Windows. Utilice «#ffffff» en su lugar.

delay = 0

El retraso predeterminado entre descargas consecutivas en segundos. El argumento puede ser un número de coma flotante para indicar un tiempo más preciso. Ver get_url_specific_delay() para implementar retrasos por URL.

description = ''

Un par de líneas que describen el contenido que descarga esta fórmula. Se usará principalmente en una interfaz gráfica que presenta una lista de fórmulas.

encoding = None

Especifica una codificación para los sitios que tienen una especificación de conjunto de caracteres incorrecta. El error más común es declarar latin1 y usar cp1252. Si es None, trata de detectar la codificación. Si es un método ejecutable, se usará con dos argumentos: el objeto de la fórmula y el código fuente para descodificar. Debe devolver el código descodificado.

extra_css = None

Especifique cualquier CSS extra que debe añadirse a los archivos HTML descargados. Será insertado en etiquetas <style>, justo antes de la etiqueta de cierre </head> sustituyendo así todos los CSS excepto los que estén declarados mediante el atributo style en etiquetas HTML individuales. Tenga en cuenta que si desea generar un archivo extra_css mediante un programa debe reemplazar el método get_extra_css(). Por ejemplo:

extra_css = '.heading { font: serif x-large }'

feeds = None

Lista de canales para descargar. Puede ser [url1, url2, ...] o [('título1', url1), ('título2', url2), ...]

filter_regexps = []

Lista de expresiones regulares que determina qué enlaces ignorar. Si está vacía no se tiene en cuenta. Usado sólo si is_link_wanted no está implementado. Por ejemplo:

filter_regexps = [r'ads\.doubleclick\.net']

eliminará todos los URL que contengan ads.doubleclick.net.

Sólo uno de entre BasicNewsRecipe.match_regexps y BasicNewsRecipe.filter_regexps deben definirse.

handle_gzip = True

Establézcalo en False para no usar transferencias comprimidas con gzip. Tenga en cuenta que algunos servidores antiguos pueden fallar con gzip.

ignore_duplicate_articles = None

Ignorar duplicados de los artículos que están presentes en más de una sección. Un artículo duplicado es un artículo que tiene el mismo título o dirección URL. Para ignorar artículos con el mismo título, establézcalo como:

ignore_duplicate_articles = {'title'}

En cambio, para utilizar las direcciones URL, establézcalo como:

ignore_duplicate_articles = {'url'}

Para hacer que coincida el título o URL, establézcalo como:

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

Mantiene sólo las etiquetas especificadas y sus descendientes. El formato para especificar una etiqueta está definido en BasicNewsRecipe.remove_tags. Si la lista no está vacía, entonces la etiqueta <body> se vaciará y se volverá a llenar con las etiquetas que coincidan con las entradas en esta lista. Por ejemplo:

keep_only_tags = [dict(id=['content', 'heading'])]

mantendrá sólo las etiquetas que posean un atributo id igual a «content» o «heading».

language = 'und'

El idioma en que están las noticias. Debe ser un código ISO-639 de dos o tres letras

masthead_url = None

De forma predeterminada, calibre utilizará una imagen predeterminada para la cabecera (sólo Kindle). Sustituya esto en la receta para proporcionar un URL que se usará como cabecera.

match_regexps = []

Lista de expresiones regulares que determina qué enlaces seguir. Si está vacía, no se tiene en cuenta. Se usa sólo si is_link_wanted no está implementado. Por ejemplo:

match_regexps = [r'page=[0-9]+']

coincidirá con todas las direcciones URLs que posean page=algún número.

Sólo uno de entre BasicNewsRecipe.match_regexps y BasicNewsRecipe.filter_regexps deben definirse.

max_articles_per_feed = 100

Número máximo de artículos para descargar de cada canal. Esto es útil sobre todo para los canales que no tienen fechas en los artículos. Para la mayoría de los canales, debe usar BasicNewsRecipe.oldest_article

needs_subscription = False

Si es True, la interfaz gráfica pedirá al usuario un nombre y una contraseña para utilizar en la descarga. Si se establece en "optional" el nombre y contraseña serán opcionales.

no_stylesheets = False

Opción útil para desactivar la carga de hojas de estilo en los sitios de Internet que tienen hojas de estilo excesivamente complejas, poco apropiadas para la conversión a formatos de libro electrónico. Si es True, las hojas de estilo no se descargan ni procesan

oldest_article = 7.0

Artículo más antiguo para descargar desde la fuente de noticias. En días.

preprocess_regexps = []

Lista de reglas de sustitución regexp para ejecutar sobre los HTML descargados. Cada elemento de la lista debe ser una tupla de dos elementos. El primer elemento de la tupla debe ser una expresión regular compilada y el segundo un método ejecutable que toma un objeto de coincidencia único y devuelve un texto para sustituir a la coincidencia. Por ejemplo:

preprocess_regexps = [
   (re.compile(r'<!--Article ends here-->.*</body>', re.DOTALL|re.IGNORECASE),
    lambda match: '</body>'),
]

eliminará todo lo que haya entre <!–Article ends here–> y </body>.

publication_type = 'unknown'

Tipo de publicación. Establecer como «newspaper», «magazine» o «blog». Si se establece en None, no se escribirá el metadato de tipo de publicación en el archivo opf.

recipe_disabled = None

Establecer a un texto no vacío para desactivar esta fórmula. El texto se utilizará como el mensaje de desactivación.

recursions = 0

Número de niveles de enlaces para seguir en las páginas de internet de los artículos

remove_attributes = []

Lista de atributos para quitar de todas las etiquetas. Por ejemplo:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

Si es True los canales vacíos se eliminan de la salida. Esta opción no tiene efecto si parse_index se reemplaza en la subclase. Está pensado sólo para las fórmulas que devuelven una lista de canales que utilizan feeds o get_feeds(). También se utiliza si se emplea la opción ignore_duplicate_articles.

remove_javascript = True

Opción útil para eliminar todas las etiquetas JavaScript del archivo HTML descargado

remove_tags = []

Lista de etiquetas para ser eliminadas. Las etiquetas especificadas se eliminan del HTML descargado. Cada etiqueta se especifica como un diccionario de la forma:

{
 name      : 'tag name',   #e.g. 'div'
 attrs     : a dictionary, #e.g. {'class': 'advertisment'}
}

Todas las claves son opcionales. Para una explicación completa de los criterios de búsqueda, véase Beautiful Soup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree] (en inglés). Un ejemplo frecuente:

remove_tags = [dict(name='div', class_='advert')]

Esto eliminará todas las etiquetas <div class=»advert»> y todos sus elementos descendientes del HTML descargado.

remove_tags_after = None

Elimina todas las etiquetas que se aparecen después de la etiqueta especificada. La forma de especificar una etiqueta se muestra en BasicNewsRecipe.remove_tags. Por ejemplo:

remove_tags_after = [dict(id='content')]

eliminará todas las etiquetas después del primer elemento con id=»content».

remove_tags_before = None

Elimina todas las etiquetas que aparecen antes de la etiqueta especificada. La forma de especificar una etiqueta se da en BasicNewsRecipe.remove_tags. Por ejemplo:

remove_tags_before = dict(id='content')

eliminará todas las etiquetas antes del primer elemento con id=»content».

requires_version = (0, 6, 0)

Versión mínima de calibre necesaria para usar esta fórmula

resolve_internal_links = False

Si es True los enlaces en los artículos descargados que apunten a otros artículos descargados se cambian para que apunten a la copia descargada del artículo en lugar de al URL original. Si lo establece en True, puede que también necesite implementar canonicalize_internal_url() para que funcione con el esquema URL del sitio web concreto.

reverse_article_order = False

Invierte el orden de los artículos de cada canal

scale_news_images = None

Dimensiones máximas (anchura, altura) para redimensionar las imágenes. Si scale_news_images_to_device es True, estas dimensiones son el tamaño de la pantalla del dispositivo establecido en el perfil de salida, a menos que no haya un perfil seleccionado, en cuyo caso se deja en el valor que se le haya asignado (de manera predeterminada None).

scale_news_images_to_device = True

Cambiar el tamaño de las imágenes para que quepan en la pantalla del dispositivo establecido por el perfil de salida. Se omite si no hay un perfil de salida seleccionado.

simultaneous_downloads = 5

Número de descargas simultáneas. Establecer en 1 si el servidor es exigente. Se reduce automáticamente a 1 si BasicNewsRecipe.delay > 0

summary_length = 500

Número máximo de caracteres en la descripción breve

template_css = '\n            .article_date {\n                color: gray; font-family: monospace;\n            }\n\n            .article_description {\n                text-indent: 0pt;\n            }\n\n            a.article {\n                font-weight: bold; text-align:left;\n            }\n\n            a.feed {\n                font-weight: bold;\n            }\n\n            .calibre_navbar {\n                font-family:monospace;\n            }\n    '

El CSS que se usa para dar el estilo a las plantillas, por ejemplo, las barras de navegación y los índices. En lugar de sustituir esta variable, puede usar extra_css en la fórmula para personalizar la apariencia.

timefmt = ' [%a, %d %b %Y]'

El texto de formato para la fecha que se muestra en la primera página. Predeterminado: Nombre_del_día, Número_del_día Nombre_del_mes Año

timeout = 120.0

Tiempo de espera para obtener archivos desde el servidor, en segundos

title = 'Fuente de noticias desconocida'

El título que se usará para el libro electrónico

use_embedded_content = None

Normalmente se intenta deducir si un canal contiene artículos completos incrustados, sobre la base de la longitud del contenido incrustado. Si es Ninguno, entonces se utiliza el método predeterminado. Si es True, se supone siempre que los canales contienen artículos incrustados y si es False, se supone siempre que los canales no contienen artículos incrustados.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

El visor de libros electrónicos

El visor de libros electrónicos

calibre incluye un visor de libros electrónicos que puede mostrar los principales formatos de libros electrónicos. El visor es altamente personalizable y posee muchas funciones avanzadas.

Iniciar el visor de libros

Desplazarse por un libro electrónico

Resaltar texto

Leer en alto

Buscar en el texto

Seguir enlaces usando sólo el teclado

Personalizar la apariencia de la experiencia de lectura

Consulta en diccionarios

Copiar texto e imágenes

Ampliar imágenes

Atajos de teclado

Contenido no redistribuible

Diseñar un libro para que funcione bien con el visor de calibre

Iniciar el visor de libros

Puede ver cualquiera de los libros de la biblioteca calibre seleccionando el libro y pulsando el botón Mostrar. Esto abrirá el libro en el visor de libros electrónicos. También puede iniciar el visor por sí mismo desde el menú Inicio en Windows. En macOS puede añadirlo al doc y lanzarlo desde ahí. En Linux puede usar el lanzador en los menús del escritorio o ejecutar la orden ebook-viewer.

Desplazarse por un libro electrónico

Puede «pasar páginas» en un libro:

Pulsando en el margen izquierdo o derecho de la página con el ratón

Pulsando las teclas espacio, av. pág., ret. pag. o las flechas

En una pantalla táctil, tocando el texto o deslizando a izquierda o derecha

Puede acceder a los controles del visor:

Pulsando con el botón derecho sobre el texto

Pulsando las teclas esc o menú

En una pantalla táctil, tocando en el tercio superior de la pantalla

El visor tiene dos modos, «paginado» y «continuo». En el modo paginado, el contenido se presenta como páginas, de manera parecida a un libro en papel. En el modo continuo el texto se presenta como un flujo continuo, de manera parecida a un navegador web. Puede cambiar entre ellos usando las Preferencias del visor, en Estructura de la página o pulsando las teclas Ctrl+M.

Marcadores

Si cierra el visor de libros electrónicos en medio de un libro, recordará dónde se detuvo la lectura y retornará a esa posición la próxima vez que abra el libro. También puede establecer marcadores en el libro mediante el botón Marcadores |bookmi| en el control del visor de libros electrónicos o pulsando Ctrl+B. Al ver libros en formato EPUB, estos marcadores se guardan realmente en el mismo archivo EPUB. Puede añadir marcadores y posteriormente, enviar el archivo a un amigo. Cuando abra el archivo, podrá ver los marcadores. Puede desactivar este comportamiento en la sección Miscelánea de las preferencias del visor.

Índice

Si el libro que está leyendo tiene definido un índice, puede acceder a él pulsando el botón Índice. Se mostrará una lista de secciones en el libro. Puede hacer pulsar en cualquiera de ellas para ir a la posición correspondiente del libro.

Navegar por ubicación

Los libros electrónicos, al contrario que los libros en papel, no tienen concepto de página. Puede acudir a posiciones concretas de un libro electrónico usando la función Ir a > Ubicación en los controles del visor.

Puede usar esta información de ubicación para hacer especificar sin ambigüedades las distintas partes de un libro al hablar con amigos o al hacer referencias al libro en otros documentos. Puede introducir estas ubicaciones en Ir a > Ubicación en los controles del visor.

Hay un URL que puede copiar al portapapeles y pegar en otros programas o documentos. Si pulsa en este URL se abrirá el libro en el visor de libros electrónicos de calibre en la ubicación actual.

Si pulsa en enlaces dentro del libro electrónico para ir a diferentes partes del libro, como una nota al final, puede utilizar los botones Atrás y Adelante en la esquina superior izquierda de los controles del visor. Estos botones se comportan igual que los de un navegador de Internet.

Modo de referencia

calibre también tiene un Modo de referencia muy útil. Puede activarlo pulsando en el botón Modo de referencia en los controles del visor. Una vez hecho esto, cada párrafo tendrá al inicio un número exclusivo, formado por los números de sección y párrafo.

Puede usar este número para hacer especificar sin ambigüedades las distantas partes de un libro al hablar con amigos o al hacer referencias al libro en otros documentos. Puede introducir estos números en la función Ir a… para ir a un lugar de referencia particular.

Resaltar texto

Cuando selecciona texto en el visor, aparece una pequeña barra emergente junto a la selección. Puede pulsar el botón de resaltado en esta barra para crear un resaltado. Puede añadir notas y cambiar el color del resaltado. En una pantalla táctil, toque prolongadamente una palabra para seleccionarla y mostrar la barra emergente. Una vez en modo de resaltado, puede modificar qué texto se selecciona usando marcadores adaptados a pantallas táctiles. Arrastre los marcadores al margen superior o inferior para desplazarse mientras selecciona. También puede mantener pulsada la tecla Mayús y pulsar, o pulsar con el botón derecho, para extender la selección, lo que es particularmente útil para selecciones que ocupan varias páginas.

Puede usar el botón Resaltados en los controles del visor para mostrar un panel independiente con una lista de todos los resaltados en el libro, ordenados por capítulo.

Puede explorar todos los resaltados en la biblioteca de calibre completa pulsando con el botón derecho el botón Mostrar y eligiendo Explorar anotaciones.

Por último, si usa el visor del navegador en el servidor de contenidos de calibre, puede sincronizar las anotaciones del visor con las del visor del navegador si en Preferencias > Miscelánea en el visor introduce el nombre de usuario del servidor de contenidos con el que sincronizar. Use el valor especial * para sincronizar con usuarios anónimos.

Leer en alto

El visor puede leer texto en alto. Para usar esta función, pulse el botón Leer en alto en los controles del visor para comenzar la lectura del libro. La palabra que se esté leyendo en cada momento se resaltará. La voz se genera a partir del texto usando los servicios de síntesis de voz del sistema operativo. Puede cambiar la voz en uso pulsando el botón con el engranaje en la barra que se muestra cuando Leer en alto está activo.

También puede leer en alto pasajes resaltados si añade el botón Leer en alto a la barra de selección en las preferencias del visor, en Comportamiento de selección.

Nota

La compatibilidad con síntesis de voz en los navegadores es muy incompleta y tiene muchos fallos, así que la calidad de Leer en alto en el visor del navegador dependerá de la calidad de la síntesis de voz en el navegador. En concreto, el resaltado de la palabra actual no funciona, y si se modifica la velocidad o la voz, la lectura volverá a empezar desde el principio.

Nota

En Linux, Leer en alto requiere que Speech Dispatcher [https://freebsoft.org/speechd] esté instalado y funcione.

Nota

En Windows, puede que no todas las voces instaladas sea visibles para el subsistema SAPI que se usa para la síntesis de voz. Existen instrucciones para hacer visibles todas las voces [https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108] (en inglés).

Buscar en el texto

El visor tiene unas posibilidades de búsqueda muy potentes. Pulse Ctrl+F para acceder a los controles del visor y pulse buscar. La forma más sencilla de búsqueda es simplemente buscar el texto que escriba en el cuadro. Las distintas formas de búsqueda se elijen en la casilla de modo de búsqueda bajo la entrada de búsqueda. Los modos disponibles son:

Contiene - El modo predeterminado y más simple. El texto introducido en el cuadro de búsqueda se busca en cualquier parte. Se ignoran todos los espacios, acentos y signos de puntuación. Por ejemplo la búsqueda Pena encontrará todos los casos siguientes: penal, pen a, pen.a y Peña. Si activa la casilla Distinguir mayúsculas y minúsculas, ya no se ignorarán espacios, acentos y signos de puntuación.

Palabras completas - Se buscan palabras completas. Por ejemplo, la búsqueda pena encontrará la palabra Peña pero no Penal. Como con las búsquedas de tipo contiene descritas arriba, se ignoran los acentos y signos de puntuación a no ser que se marque la casilla Distinguir mayúsculas y minúsculas.

Palabras cercanas - Se buscan palabras completas que están cercanas entre sí. Por ejemplo, la búsqueda calibre estupendo encontrará pasajes donde las palabras calibre y estupendo aparezcan separadas por sesenta caracteres o menos. Para cambiar el número de caracteres añada el nuevo número al final de la lista de palabras. Por ejemplo, calibre estupendo impresionante 120 encontrará pasajes donde las tres palabras aparezcan separadas por 120 caracteres o menos. Tenga en cuenta que los acentos y signos de puntuación no se ignoran en estas búsquedas.

Expresión regular - Se interpreta el texto de búsqueda como una «expresión regular». Para saber más sobre el uso de expresiones regulares, vea el cursillo.

Seguir enlaces usando sólo el teclado

El visor de libros electrónicos tiene un modo directo que le permite seguir enlaces en el texto sin usar el ratón. Pulse la tecla Alt+F y se resaltarán todos los enlaces en la pantalla actual con un número o letra sobre ellos. Pulse la tecla correspondiente para activar el enlace. Si pulsa la tecla Esc se aulará el modo directo sin seleccionar ningún enlace.

Si hay más de 35 enlaces en la pantalla algunos de ellos tendrá varias letras; en ese caso, pulse la primera y la segunda, o la primera y luego Intro para activar un enlace. También puede usar la tecla Retroceso para corregir un error de tecleo.

Personalizar la apariencia de la experiencia de lectura

Puede cambiar el tamaño de letra al vuelo usando Tamaño de letra en los controles del visor o ctrl++ o ctrl+-, o pulsando la tecla ctrl y usando la rueda del ratón.

Los colores pueden cambiarse en la sección Colores de las preferencias del visor.

Puede cambiar el número de páginas que se muestran en pantalla, así como los márgenes de página, en Estructura de la página en las preferencias del visor.

Puede mostrar encabezados y pies de página personalizados, tales como el tiempo restante de lectura, el título del capítulo actual, posición del libro, etc. en la sección Encabezados y pies de página de las preferencias del visor.

Puede lograr una personalización más avanzada mediante la configuraciónes de Estilos. Aquí puede especificar una imagen de fondo para mostrar tras el texto y también una hoja de estilos opcional que se aplicará a todos los libros. Por medio de ella puede hacer cosas como cambiar estilos de párrafo, la justificación del texto, etc. Para ver ejemplos de hojas de estilo personalizadas utilizadas por otros usuarios de calibre, vea los foros [https://www.mobileread.com/forums/showthread.php?t=51500] (en inglés).

Consulta en diccionarios

Puede buscar el significado de las palabras en el libro actual pulsando dos veces o con un toque largo sobre la palabra, y luego pulsando en el botón de consulta que parece una biblioteca.

Copiar texto e imágenes

Puede seleccionar texto e imágenes arrastrando el contenido con el ratón y luego pulsando con el botón derecho y seleccionando Copiar para copiar en el portapapeles. El material copiado se puede pegar en otra aplicación como texto sin formato e imágenes.

Ampliar imágenes

Puede mostrar una imagen ampliada a su tamaño completo en una ventana aparte pulsando dos veces o usando un toque largo sobre ella. También puede pulsar con el botón derecho y elegir Ver imagen.

Atajos de teclado

El visor tiene muchos atajos de teclado, como el resto de calibre. Los atajos pueden personalizarse en las Preferencias del visor. Los atajos predeterminados se muestran a continuación:

Atajos de teclado para el visor de calibre

Atajo de teclado

Acción

Inicio, Ctrl+Flecha arriba, Ctrl+Flecha izquierda

Desplazarse al inicio del archivo actual en un libro de varios archivos

Ctrl+Inicio

Desplazarse al principio del libro

Ctrl+Fin

Desplazarse al final del libro

Fin, Ctrl+Flecha abajo, Ctrl+Flecha derecha

Desplazarse al final del archivo actual en un libro de varios archivos

Flecha arriba

Desplazarse hacia atrás, suavemente en modo continuo y por pantallas en modo paginado

Flecha abajo

Desplazarse hacia delante, suavemente en modo continuo y por pantallas en modo paginado

Flecha izquierda

Desplazarse hacia la izquierda un poco en modo continuo y una página en modo paginado

Flecha derecha

Desplazarse hacia la derecha un poco en modo continuo y una página en modo paginado

RePág, Mayús+Barra espaciadora

Desplazarse hacia atrás por pantallas

AvPág, Barra espaciadora

Desplazarse hacia delante por pantallas

Ctrl+RePág

Desplazarse a la sección anterior

Ctrl+AvPág

Desplazarse a la siguiente sección

Alt+Flecha izquierda

Atrás

Alt+Flecha derecha

Adelante

Ctrl+T

Conmutar índice

Ctrl+S

Leer en alto

Alt+P

Guardar configuraciones rápidamente creando y activando perfiles

Alt+F

Seguir enlaces con el teclado

Ctrl+C

Copiar al portapapeles

Alt+C

Copiar ubicación actual al portapapeles

Ctrl+Mayús+C

Copiar la ubicación actual como un URL de tipo calibre:// al portapapeles

/, Ctrl+F, Cmd+F

Iniciar búsqueda

F3, Intro

Buscar siguiente

Mayús+F3, Mayús+Intro

Buscar anterior

Ctrl+Más, Meta+Más

Aumentar el tamaño de letra

Ctrl+Menos, Meta+Menos

Reducir el tamaño de letra

Ctrl+0

Restaurar el tamaño de letra predeterminado

Ctrl+]

Aumentar el número de páginas por pantalla

Ctrl+[

Reducir el número de páginas por pantalla

Ctrl+Alt+C

Hacer que el número de páginas por pantalla sea automático

F11, Ctrl+Mayús+F

Conmutar pantalla completa

Ctrl+M

Alternar entre modo paginado y modo continuo para la distribución del texto

Ctrl+W

Conmutar la barra de desplazamiento

Ctrl+X

Conmutar el modo de referencia

Ctrl+B

Mostrar u ocultar marcadores

Ctrl+Alt+B

Nuevo marcador

Ctrl+N, Ctrl+E

Mostrar los metadatos del libro

Ctrl+Alt+F5, Ctrl+Alt+R

Volver a cargar libro

Ctrl+Mayús+Flecha derecha

Modificar la selección actual una palabra hacia adelante

Ctrl+Mayús+Flecha izquierda

Modificar la selección actual una palabra hacia atrás

Mayús+Flecha derecha

Modificar la selección actual un carácter hacia adelante

Mayús+Flecha izquierda

Modificar la selección actual un carácter hacia atrás

Mayús+Flecha abajo

Modificar la selección actual una línea hacia adelante

Mayús+Inicio

Extender la selección actual hasta el inicio de la línea

Mayús+Fin

Extender la selección actual hasta el final de la línea

Ctrl+A

Seleccionar todo

Mayús+Flecha arriba

Modificar la selección actual una línea hacia atrás

Ctrl+Mayús+Flecha abajo

Modificar la selección actual un párrafo hacia adelante

Ctrl+Mayús+Flecha arriba

Modificar la selección actual un párrafo hacia atrás

Esc, Tecla de menú

Mostrar los controles del visor de libros electrónicos

Ctrl+Coma, Ctrl+Esc, Meta+Esc, Meta+Coma

Mostrar las preferencias del visor de libros electrónicos

Ctrl+G, ;, :

Ir a una ubicación o posición específica del libro

Ctrl+Barra espaciadora

Conmutar desplazamiento automático

Alt+Flecha arriba

Desplazamiento automático más rápido

Alt+Flecha abajo

Desplazamiento automático más lento

Ctrl+I

Mostrar u ocultar el inspector

Ctrl+L

Mostrar u ocultar el panel de consulta de palabra

Ctrl+Q (Cmd+Q en macOS)

Salir

Ctrl+P

Imprimir libro a PDF

Ctrl+F11

Conmutar la barra de herramientas

Ctrl+H

Conmutar el panel de resaltados

Ctrl+D

Modificar este libro

Contenido no redistribuible

Algunos libros tienen contenido muy ancho que no puede dividirse entre páginas. Por ejemplo, tablas o etiquetas <pre>. En tales casos deberá cambiar el visor al modo continuo pulsando Ctrl+M para leer el contenido. Alternativamente, puede añadir el siguiente CSS a la sección Estilos de las preferencias del visor para forzar el visor a dividir las líneas de etiquetas <pre>:

code, pre { white-space: pre-wrap }

Diseñar un libro para que funcione bien con el visor de calibre

El visor de calibre asigna la clase is-calibre-viewer al elemento raíz, de manera que puede escribir reglas CSS que sólo se apliquen a él. Además, el visor asigna las siguientes clases al elemento body:

body.calibre-viewer-dark-colors

Se asigna cuando se usa un esquema de colores oscuro

body.calibre-viewer-light-colors

Se asigna cuando se usa un esquema de colores claro

body.calibre-viewer-paginated

Se asigna en modo paginado

body.calibre-viewer-scrolling

Se asigna en modo continuo (no paginado)

body.calibre-footnote-container

Se asigna al mostrar una nota emergente

Por último, puede usar los colores del esquema de colores de calibre mediante variables CSS [https://developer.mozilla.org/es/docs/Web/CSS/Using_CSS_custom_properties]. El visor de calibre define las siguientes variables: --calibre-viewer-background-color (fondo), --calibre-viewer-foreground-color (primer plano) y opcionalmente --calibre-viewer-link-color (enlaces) en esquemas de color que definen un color para enlaces.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Conversión de libros

Conversión de libros

calibre tiene un sistema de conversión diseñado para que sea fácil de usar. Normalmente no tiene más que añadir un libro a calibre, pulsar en convertir y calibre intentará generar una salida tan parecida como sea posible a la entrada. Sin embargo, calibre acepta un gran número de formatos de entrada, y no todos son tan adecuados como otros para convertirse a otros formatos. En el caso de los formatos menos adecuados, o si simplemente desea tener mayor control sobre el sistema de conversión, calibre tiene muchas opciones para ajustar los detalles del sistema de conversion. Tenga en cuenta que el sistema de conversión de calibre no es un sustituto para un editor completo de libros electrónicos. Para modificar libros le recomiendo que los convierta primero a EPUB o AZW3 con cailbre y luego use la función Modificar libro para darle la forma perfecta. Después puede usar el libro modificado como entrada para otros formatos en calibre.

Este documento se referirá principalmente a las opciones de conversión que se encuentran en el cuadro de diálogo de conversión, mostrado a continuación. Todas estas opciones también están disponibles en la interfaz de línea de órdenes para la conversión, documentada en ebook-convert. En calibre, puede obtener ayuda sobre cualquier opción manteniendo el cursor sobre ella, aparecerá una ayuda emergente que describe la opción.

Índice

Introducción

Apariencia

Configuración de página

Procesado heurístico

Buscar y sustituir

Detección de estructura

Índice

Usar imágenes como títulos de capítulo al convertir documentos de entrada HTML

Usar atributos de etiquetas para suministrar el texto de las entradas del índice

Cómo se establecen y guardan las opciones de conversión

Consejos para formatos específicos

Introducción

Lo primero que debe comprender sobre el sistema de conversión es que está diseñado como una serie de operaciones. Esquemáticamente, es alg así:

El formato de entrada se convierte primero a XHTML con el complemento de entrada apropiado. Después este HTML se transforma. En el último paso, el XHTML procesado se convierte al formato de salida especificado con el complemento de salida adecuado. Los resultados de la conversión pueden variar enormemente según el formato de entrada. Algunos formatos de entrada se convierten mucho mejorque otros. Una lista de los mejores formatos de origen para la conversión está disponible aquí.

Todo el trabajo tiene lugar en las transformaciones que operan sobre la salida XHTML. Hay varias transformaciones, por ejemplo, para insertar los metadatos del libro en una página al inicio del libro, para detectar encabezados de capítulo y crear un índice automáticamente, para ajustar los tamaños de letra proporcionalmente, etc. Es importante recordar que todas las transformaciones actúan sobre el XHTML creado por el complemento de entrada, no sobre el archivo de entrada en sí. Así, por ejemplo, si pide a calibre que convierta un archivo RTF a EPUB, primero lo convertirá a XHTML internamente, se aplicarán las distintas transformaciones sobre el XHTML y después el complemento de salida creará el archivo EPUB, generando automáticamente todos los metadatos, índice, etc.

Puede ver este proceso en acción usando la opción de depuración Icono de depuración. Simplemente especifique la ruta de acceso a una carpeta para la salida de depuración. Durante la conversión, calibre guardará el XHTML generado por las distintas etapas del proceso de conversión en subcarpetas separadas. Las cuatro subcarpetas son:

Etapas del proceso de conversión

Carpeta

Descripción

input

Contiene el HTML creado por el complemento de entrada. Úselo para depurar el complemento de entrada.

parsed

El resultado del preprocesado y conversión a XHTML de la salida del complemento de entrada. Úselo para depurar la detección de estructura.

structure

Posterior a la detección de estructura, pero anterior al aplanado de CSS y conversión de tamaños de letra. Úselo para depurar la conversión de tamaños de letra y las transformaciones de CSS.

processed

Justo antes de que el libro pase al complemento de salida. Úselo para depurar el complemento de salida.

Si desea modificar el documento de salida antes de que calibre lo convierta, lo mejor es modificar los archivos en la subcarpeta input, comprimirlos en un archivo ZIP y usar éste como formato de entrada para las subsiguientes conversiones. Para hacer esto use el cuadro de diálogo Modificar metadatos para añadir el ZIP como formato para el libro y entonces, en la esquina superior izquierda del cuadro de diálogo de conversión, seleccione ZIP como formato de entrada.

Este documento se ocupará principalmente de las distintas transformaciones que operan sobre el XHTML intermedio y cómo controlarlas. Al final hay algunos consejos específicos para cada formato de entrada o salida.

Apariencia

Índice

Tipos de letra

Texto

Distribución

Estilos

Transformar estilos

Transformar HTML

Las opciones de este grupo controlan varios aspectos del aspecto del libro electrónico convertido.

Tipos de letra

Una de las ventajas de la lectura electrónica es la posibilidad de ajustar los tamaños de letra según las necesidades individuales y las condiciones de iluminación. calibre incluye sofisticados algoritmos para asegurar que todos los libros que crea tienen tamaños de letra coherentes, independientemente de los tamaños de letra especificados en el documento de entrada.

El tamaño de letra base de un documento es el tamaño de letra más común en el documento, es decir, el tamaño del texto principal del documento. Cuando especifica un Tamaño de letra base, calibre redimensiona automáticamente todos los tamaños de letra del documento proporcionalmente, de manera que el tamaño más común pasa a ser el tamaño base especificado y el resto de los tamaños de letra se redimensionan adecuadamente. Si elige un tamaño de letra base mayor hará que todos los textos del documento sean mayores. Al establecer un tamaño de letra base, para obtener mejores resultados debe establecer también la clave de tamaño de letra.

Normalmente calibre elegirá automáticamente un tamaño de letra base adecuado para el perfil de salida que haya seleccionado (ver Configuración de página). Si el tamaño predeterminado no le satisface, puede especificar otro aquí.

La opción Clave de tamaño de letra le permite especificar cómo se redimensionan los tamaños de letra que no son el base. El algoritmo de redimensionado funciona usando una clave de tamaño de letra, que no es más que una lista de tamaños de letra separados por comas. La clave de tamaño de letra le dice a calibre cuántos «escalones» mayor o menor debe ser un tamaño determinado comparado con el tamaño de letra base. La idea es que debe haber un número limitado de tamaños de letra en un documento. Por ejemplo, un tamaño para el texto principal, un par de tamaños para distintos niveles de encabezados, y un par de tamaños para subíndices o superíndices, o notas a pie de página. La clave de tamaño de letra le permite a calibre clasificar los tamaños de letra en los documentos de entrada en distintas «casillas» que corresponden a los distintos tamaños de letra lógicos.

Veámoslo con un ejemplo. Supongamos que el documento de origen que estamos convirtiendo lo produjo alguien con una vista excelente y tiene un tamaño de letra base de 8pt. Esto significa que el texto principal del documento tiene un tamaño de 8 pt, mientras que los encabezados son algo mayores (digamos 10pt y 12pt) y las notas a pie de página algo menores (6pt). Si usamos las siguientes configuraciones:

Base font size : 12pt
Font size key  : 7, 8, 10, 12, 14, 16, 18, 20

El documento de salida tendrá un tamaño de letra base de 12pt, encabezados de 14pt y 16pt y notas a pie de página de 8pt. Ahora supongamos que queremos que los encabezados mayores destaquen más y hacer las notas a pie de página algo mayores también. Para conseguir esto, debemos cambiar la clave de tamaño de letra a:

New font size key : 7, 9, 12, 14, 18, 20, 22

Los encabezados mayores serán ahora de 18pt, mientras que las notas a pie de página serán de 9pt. Puede ajustar estos valores para tratar de optimizarlos usando el asistente de redimensionado de tamaños de letra, al que se puede acceder pulsando en el pequeño botón junto a la opción Clave de tamaño de letra.

También puede desactivarse aquí todo el redimensionado de tamaños de letra en la conversión, si dese mantener los tamaños de letra del documento de entrada.

Una opción relacionada es Altura de línea. La altura de línea controla la separación vertical entre renglones. De manera predeterminada (una altura de línea de 0), no se realiza ninguna manipulación de alturas de línea. Si especifica otro valor, se incluirá una altura de línea en todos los lugares que no especifique su propia altura de línea. Esto es una herramienta bastante tosca y debería usarla en contadas ocasiones. Si quiere ajustar la altura de línea en alguna sección particular de la entrada, es mejor usar el CSS adicional.

En esta sección también puede hacer que calibre incruste los tipos de letra a los que se hace referencia en el libro. Esto permitirá que los tipos de letra funcionen en los dispositivos de lectura incluso si no están disponibles en el dispositivo.

Texto

El texto puede estar justificado o no. El texto justificado tiene espacios adicionales entre las palabras para obtener un margen derecho continuo. Algunas personas prefieren texto justificado, otras no. Normalmente, calibre mantiene la justificación del documento original. Si quiere modificarlo, utilice la opción Justificación del texto en esta sección.

Tambien pude usar la opción Mejorar puntuación de calibre, que sustituirá las comillas rectas, guiones y puntos suspensivos por sus variantes tipográficamente correctas. Tenga en cuenta que este algoritmo no es perfecto, por lo que debería revisar el resultado. La operación inversa, Simplificar puntuación, también está disponible.

Por último está la Codificación de entrada. Los documentos antiguos a veces no especifican la codificación de caracteres. Al convertirlos esto puede resultar en la corrupción de caracteres no ASCII, como las letras acentuadas o las comillas tipográficas. calibre intenta detectar automáticamente la codificación de caracteres del documento de entrada, pero no siempre tiene éxito. Puede forzar una codificación concreta con esta opción. cp1252 es una codificación frecuente en documentos creados con software de Windows. También debería leer ¿Cómo convierto un archivo que contiene caracteres «exóticos» (acentuados, no latinos, comillas tipográficas, etc.)? para saber más sobre posibles problemas de codificación.

Distribución

Normalmente, los párrafos en XHTML se muestran con un espacio entre ellos y sin sangría en el primer renglón. calibre tiene un par de opciones para controlar esto. Eliminar el espacio entre párrafos fuerza que no haya separación entre párrafos. También establece la sangría en 1.5em (se puede cambiar) para indicar el inicio de cada párrafo. Insertar una línea en blanco entre párrafos hace lo contrario, asegurándose de que los párrafos están separados por el espacio correspondiente a un renglón. Ambas opciones son my generales, añaden o eliminan el espacio para todos los párrafos (técnicamente, para las etiquetas <p> y <div>). Esto es así para que pueda activar la opción y tener la seguridad de que hace lo que anuncia, sin importar lo enrevesado que pueda ser el archivo de entrada. La única excepción es cuando el archivo de entrada usa saltos de línea como espacio entre párrafos.

Si desea eliminar los espaciados entre todos los párrafos, exceptuando algunos, no utilice estas opciones. En lugar de ello añada el siguiente código CSS en CSS adicional:

p, div { margin: 0pt; border: 0pt; text-indent: 1.5em }
.spacious { margin-bottom: 1em; text-indent: 0pt; }

Después, en el documento de origen, marque los párrafos que necesitan espaciado con class=»spacious». Si el documento de entrada no está en formato HTML, use la opción de depuración, mencionada en la Introducción para obtener HTML (use la subcarpeta input).

Otra opción útil es Linealizar tablas. Algunos documentos mal diseñados usan tablas para controlar la disposición del texto en la página. Cuando se convierten estos documentos suelen dar lugar a texto que se sale de la página y otros problemas. Esta opción extrae el contenido de las tablas y lo presenta de manera lineal. Tenga en cuenta que esta opción linealiza todas las tablas, así que úsela sólo cuando esté seguro de que el documento de entrada no usa tablas con fines legítimos, como, por ejemplo, para presentar información tabulada.

Estilos

La opción CSS adicional le permite especificar código CSS arbitrario que se aplicará a todos los archivos HTML de la entrada. Este código CSS se aplica con muy alta prioridad, por lo que puede reemplazar la mayor parte del código CSS en el documento de entrada. Puede usar esta opción para refinar la presentación o dsiposición del documento. Por ejemplo, si desea que todos los párrafos de la clase endnote estén alineados a la derecha, añada:

.endnote { text-align: right }

o si desea cambiar la sangría de todos los párrafos:

p { text-indent: 5mm; }

CSS adicional es una opción muy potente, pero necesita entender cómo funciona el código CSS para sacarle el máximo partido. Puede utilizar la opción de depuración mencionada anteriormente para ver el código CSS presente en el documento de entrada.

Una opción más sencilla es usar Filtrar información de estilo. Esto le permite eliminar todas las propiedades CSS de los tipos especificados del documento. Por ejemplo, puede eliminar todos los colores o tipos de letra.

Transformar estilos

Esta es la función más potente relacionada con los estilos. Puede usarla para definir reglas que modificarán los estilos según distintas condiciones. Por ejemplo, puede cambiar todo el color verde a azul, o eliminar todas las negritas del texto o asignar un color determinado a todos los encabezados, etc.

Transformar HTML

Parecido a la transformación de estilos, pero le permite hacer cambios en el código HTML del libro. Puede sustituir una etiqueta por otra, añadir clases u otros atributos a etiquetas según su contenido, etc.

Configuración de página

Las opciones de configuración de página son para controlar la disposición en la pantalla, como márgenes y tamaño de pantalla. Hay opciones para establecer márgenes de página, que usará el complemento de salida si el formato de salida especificado admite márgenes de página. Además, debería seleccionar un perfil de entrada y un perfil de salida. Ambos perfiles se encargan básicamente de interpretar las dimensiones en los documentos de entrada o salida, los tamaños de pantalla y las claves predeterminadas de tamaño de letra.

Si sabe que el archivo concreto que va a convertir está pensado para un dispositivo o programa particular, elija el correspondiente perfil de entrada, en caso contrario elija simplemente el perfl de entrada predeterminado. Si sabe que los archivos producidos van a usarse con un tipo de dispositivo específico, elija el perfil de salida correspondiente. En caso contrario, elija el perfil de salida genérico. Si está convirtiendo a MOBI o AZW3, casi siempre querrá usar uno de los perfiles de salida Kindle. Si no, lo mejor para los lectores modernos suele ser elegir el perfil de salida Tinta electrónica genérico alta definición.

El perfil de salida también controla el tamaño de pantalla. Esto causará, por ejemplo, que las imágenes se redimensionen automáticamente para adaptarse a la pantalla en algunos formatos de salida. Por lo tanto elija el perfil de un dispositivo que tenga una pantalla de tamaño similar a su dispositivo.

Procesado heurístico

El procesado heurístico suministra una variedad de funciones que pueden utilizarse para detectar y corregir problemas usuales en documentos con formato deficiente. Utilice estas funciones si el documento de entrada tiene este problema. Puesto que estas funciones se basan en patrones comunes, sea consciente de que en algunos casos una opción puede degenerar en peores resultados, así que úselas con precaución. Como ejemplo, varias de estas opciones eliminarán todos los espacios duros, o puede incluir falsos positivos relativos a la función.

Activar el procesado heurístico

Esta opción activa la etapa de procesado heurístico de calibre en el proceso de conversión. Debe estar habilitada para que se apliquen varias subfunciones

Unir líneas

Si activa esta opción, calibre intentará detectar y corregir saltos de línea forzados en el documento usando pistas como la puntuación y la longitud del renglón. calibre primero intentará detectar si existen saltos de línea forzados, si no es así calibre no intentará unir renglones. Puede reducir el factor de unión de líneas si quiere «forzar» a calibre a unir renglones.

Factor de unión de líneas

Esta opción controla el algoritmo que usa calibre para eliminar los saltos de línea forzados. Por ejemplo, si el valor de esta opción es 0.4, eso significa que calibre eliminará los saltos de línea de los renglones cuya longitud sea menor del 40% de todos los renglones del documento. Si el documento tiene sólo algunos saltos de línea que necesitan la corrección, este valor debería reducirse a algo entre 0.1 y 0.2.

Detectar y marcar cabeceras y subcabeceras de capítulos sin formato

Si el documento no posee cabeceras de capítulo y títulos con un formato diferente del resto del texto, calibre puede usar esta opción para intentar detectarlos e incluirlos en etiquetas de encabezado. Las etiquetas <h2> se usan para cabeceras de capítulo; las etiquetas <h3> se usan para cualquier título que se detecte.

Esta función no creará un índice, pero en muchos casos hará que la detección automática de capítulos predeterminada de calibre detecte los capítulos correctamente y cree un índice. Ajuste la expresión XPath en Detección de estructura si no se crea automáticamente un índice. Si no se usan otras cabeceras en el documento, especificar //h:h2 como expresión XPath será la forma más fácil de crear un índice para el documento.

Las cabeceras insertadas no poseen formato, para aplicarle uno utilice la opción CSS adicional en la sección de configuración :guilabel`Apariencia`. Por ejemplo, para centrar las etiquetas de cabecera, utilice lo siguiente:

h2, h3 { text-align: center }

Renumerar secuencias de etiquetas <h1> o <h2> para evitar divisiones

Algunas editoriales utilizan varias etiquetas <h1> o <h2> consecutivas par dar formato a las cabeceras de capítulo. La configuración de conversión predeterminada de calibre hará que tales títulos se divididan en varias partes. Esta opción renumerará las etiquetas de cabecera para evitar la división.

Borrar líneas en blanco entre párrafos

Esta opción hace que calibre analice las líneas en blanco incluidas en el documento. Si todos los párrafos están separados por líneas en blanco, calibre eliminará todos esos párrafos en blanco. Varias líneas en blanco consecutivas se considerarán como saltos de escena y se mantendrán como un único párrafo. Esta opción se diferencia de Eliminar el espacio entre párrafos en Apariencia en que introduce cambios en el código HTML, mientras que la otra opción sólo modifica los estilos del documento. Esta opción también puede eliminar párrafos que se introdujeron con la opción de calibre Insertar líneas en blanco.

Asegurar que los cambios de escena tienen un formato consistente

Con esta opción calibre intentará detectar marcadores de cambio de escena comunes y se asegurará de que estén centrados. A los marcadores de cambio de escena «implícitos», es decir, cambios de escena definidos sólo por espacio adicional, se les aplica un estilo para evitar que coincidan con saltos de página.

Sustituir cambios de escena

Si esta opción está activada, calibre sustituirá los marcadores de cambio de escena con el texto de sustitución especificado por el usuario. Tenga en cuenta que algunos caracteres ornamentales pueden no ser compatibles con todos los dispositivos de lectura.

En general, debería evitar usar etiquetas HTML, calibre ignorará cualquier etiqueta y usará marcas predefinidas. Las etiquetas <hr/> (líneas horizontales) e <img> son excepciones. Las líneas horizontales pueden especificarse también con estilos, si decide incluir un estilo, asegurése de que tenga la opción «width», en caso contrario la información de estilo será ignorada. Las etiquetas de imagen pueden usarse, pero calibre no ofrece la posibilidad de añadir imágenes durante la conversión, esto debe hacerse posteriormente usando la función Modificar libro.

Ejemplo de etiqueta de imagen (guarda la imagen dentro de la carpeta «Images» dentro del EPUB después de la conversión):

<img style=»width:10%» src=»../Images/scenebreak.png» />

Ejemplo de barra horizontal con estilos:

<hr style=»width:20%;padding-top: 1px;border-top: 2px ridge black;border-bottom: 2px groove black;»/>

Eliminar los guiones innecesarios

calibre analizará todo el contenido con guiones en el documento si se activa esta opción. El propio documento se usa como diccionario para el análisis. Esto permite a calibre eliminar guiones en el documento con precisión para cualquier palabra en cualquier idioma, incluso en palabras inventadas u oscuros términos científicos. La principal desventaja es que las palabras que aparezcan una sola vez en el documento no se cambiarán. El análisis tienen lugar en dos pasadas, la primera pasada analiza los finales de línea. Las líneas se unirán sólo si la palabra existe en el documento con o sin guión. La segunda pasada analiza todas las palabras con guión en el documento, los guiones se eliminan si la palabra existe sin guión en algún otro lugar del documento.

Poner en cursiva palabras y patrones habituales

Si se activa, calibre buscará palabras y patrones habituales que marcan las cursivas, y los pondrá en cursiva. Algunos ejemplos son convenciones comutes de texto com ~palabra~ o frases que generalmente (en inglés) van en cursiva, como «etc.» o «et cetera».

Sustituir el sangrado mediante caracteres por sangrado CSS

Algunos documentos establecen las sangrías mediante espacios duros. Cuando se activa esta opción, calibre intenta detectar este tipo de formato y lo convierte a una sangría del 3% usando CSS.

Buscar y sustituir

Estas opciónes son útiles principalmente para la conversión de documentos PDF u OCR, aunque también pueden usarse para corregir muchos problemas específicos. Como ejemplo, algunas conversiones pueden dejar encabezados o pies de página en el texto. Estas opciones usan expresiones regulars para tratar de detectar encabezados, pies de página u otro texto arbitrario y eliminarlos o sustituirlos. Recuerde que operan sobre el código XHTML intermedio producido durante la conversión. Existe un asistente que le ayudará a personalizar las expresiones regulares para cada documento. Pulse sobre la varita mágica junto al cuadro de expresión, y pulse el botón Prueba tras completar una expresión regular. Las coincidencias se resaltarán en amarillo.

La búsqueda funciona utilizando una expresión regular Python. Todo el texto de la coincidencia se elimina del documento o se stituye utilizando el patrón de sustitución. El patrón de sustitución es opcional, si se deja en blanco el texto de la coincidencia se borrará del documento. Puede aprender más acerca de las expresiones regulares y su sintaxis en Todo acerca de cómo utilizar expresiones regulares en calibre.

Detección de estructura

La detección de estructura significa que calibre hace lo que puede para detectar elementos estructurales en el documento de entrada, cuando no tienen una especificación apropiada. Por ejemplo, capítulos, saltos de página, encabezados, pies de página, etc. Como puede imaginar, este proceso varía mucho de un libro a otro. Por fortuna, calibre tiene opciones potentes para controlarlo. Con la potencia viene la complejidad, pero si se toma el tiempo de aprender la complejidad, encontrará que bien vale el esfuerzo.

Capítulos y saltos de página

calibre tiene dos grupos de opciones para detección de capítulos e insertar saltos de página. Esto puede ser algo confuso en ocasiones, ya que calibre insertará un salto de página antes de los capítulos detectados, además de en las ubicaciones detectadas en la opción de salto de página. El motivo es que a menudo existen ubicaciones donde hay que insertar un salto de página sin que haya cambio de capítulo. Además, existe la opción de incluir los capítulos detectados en el índice generado automáticamente.

calibre usa XPath, un potente lenguaje que permite al usuario especificar límites de capítulo o saltos de página. XPath puede ser un poco intimidante al principio, pero por suerte existe un Cursillo de XPath en el Manual de usuario. Recuerde que la detección de estructura actúa sobre el XHTML intermedio producido durante el proceso de conversión. Use la opción de depuración descrita en Introducción para deducir la configuración adecuada para un libro concreto. También hay un botón para un asistente de XPath que le ayudará a generar expresiones XPath sencillas.

De manera predeterminada, calibre utiliza la siguiente expresión detectar capítulos:

//*[((name()='h1' or name()='h2') and re:test(., 'chapter|book|section|part\s+', 'i')) or @class = 'chapter']

La expresión es algo compleja, porque intenta tener en cuenta distintos casos comunes simultáneamente. Lo que significa es que calibre supondrá que los capítulos empiezan en etiquetas <h1> o <h2> que contengan alguna de las palabras chapter, book, section o part o que tengan el atributo class=»chapter».

Una opción relacionada es Marca de capítulo, que le permite controlar lo que hace calibre cuando detecta un capítulo. De manera predeterminada, insertará un salto de página antes del capítulo. Puede hacer que inserte una linea horizontal además o en lugar del salto de página. También puede hacer que no haga nada.

La configuración predeterminada para la detección de saltos de página es:

//*[name()='h1' or name()='h2']

lo que significa que, de manera predeterminada, calibre insertará saltos de página antes de cada etiqueta <h1> y <h2>.

Nota

Las expresiones predeterminadas pueden cambiar, dependiendo del formato de entrada de la conversión.

Miscelánea

Hay algunas opciones más en esta sección.

Insertar metadatos en una página al principio del libro

Uno de los mejores detalles de calibre es que permite mantener metadatos muy completos en todos los libros, por ejemplo, una calificación, etiquetas, comentarios, etc. Esta opción creará una página con todos estos metadatos y la insertará en el libro electrónico convertido, normalmente después de la portada. Piénse en ello como una forma de crear una sobrecubierta personalizada.

Eliminar la primera imagen

Algunas veces, el documento de origen que está convirtiendo incluye la portada como parte del libro, en lugar de como una portada separada. Si además especifica una portada en calibre, el libro convertido tendrá dos portadas. Esta opción simplemente eliminará la primera imagen del documento de origen, asegurando que el libro convertido posea una sola portada, la especificada en calibre.

Índice

Cuando el documento de entrada tiene un índice en los metadatos, calibre lo usará sin más. Sin embargo, algunos formatos antiguos no admiten índices basados en metadatos y algunos documentos no lo contienen. En estos casos, las opciones de esta sección pueden ayudarle a generar un índice automáticamente en el libro convertido, basado en el contenido del documento de entrada.

Nota

Puede ser un poco complicado obtener exactamente el resultado correcto con estas opciones. Si prefiere crear o modificar el índice a mano, convierta el libro al formato EPUB o AZW3 y marque la casilla en la parte inferior de la sección Índice del cuadro de dialogo de conversión que dice Ajustar manualmente el índice al finalizar la conversión. Esto ejecutará la herramienta de modificación de índice después de la conversión. Esta herramienta le permite crear entradas en el índice sin más que pulsar en la ubicación del libro adonde quiere que apunte la entrada. También puede usar el Editor del índice sin realizar ninguna conversión. Vaya a Preferencias > Interfaz > Barras de herramientas y añada Modificar el índice a la barra de herramientas principal. Despues seleccione el libro que quiera modificar y pulse en el botón Modificar el índice.

La primera opción es Forzar el uso del índice generado automáticamente. Si activa esta opción calibre reemplazará cualquier índice que encuentre en los metadatos del documento de entrada por uno generado automáticamente.

De manera predeterminada, para la creación automática del índice, calibre comienza añadiendo los capítulos detectados. Puede aprender cómo personalizar la detección de capítulos en la sección Detección de estructura más arriba. Si no quiere incluir los capítulos detectados en el índice generado, marque la opción No añadir capítulos detectados al índice.

Si el número de capítulos detectados es menor que Umbral de capítulos, calibre añadirá los enlaces que encuentre en el documento de entrada al índice. Esto suele funcionar porque muchos documentos incluyen un índice con enlaces al principio. La opción Número de enlaces para añadir al índice puede usarse para controlar este comportamiento. Si se pone a cero, no se añadirá ningún enlace. Si es un número mayor que cero, ése será el número máximo de enlaces que se añada.

calibre filtrará automáticamente duplicados del índice generado. Sin embargo, hay algunas otras entradas que puede querer eliminar del índice, lo que puede conseguir usando la opción Filtro para el índice. Se trata de una expresión regular que se comparará con las entradas del índice generado. Cualquier entrada que coincida se eliminará. Por ejemplo, para eliminar todas las entradas con título «Next» o «Previous» use:

Next|Previous

Las opciones Índice de nivel 1, 2, 3 le permiten crear un índice sofisticado con varios niveles. Son expresiones XPath que se comparan con el XHTML intermedio producido por el proceso de conversión. Vea Introducción para saber cómo acceder a este XHTML. Lea también el Cursillo de XPath para aprender como construir expresiones XPath. Junto a cada opción hay un botón que ejecuta un asistente para ayudarle a crear expresiones XPath básicas. El siguiente ejemplo sencillo muestra cómo usar estas opciones.

Supongamos que tiene un documento de entrada que da lugar a un XHTML como éste:

<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title>Sample document</title>
    </head>
    <body>
        <h1>Chapter 1</h1>
        ...
        <h2>Section 1.1</h2>
        ...
        <h2>Section 1.2</h2>
        ...
        <h1>Chapter 2</h1>
        ...
        <h2>Section 2.1</h2>
        ...
    </body>
</html>

Entonces configuramos las opciones como:

Level 1 TOC : //h:h1
Level 2 TOC : //h:h2

Esto dará lugar a un índice de dos niveles generado automáticamente que tendrá esta estructura:

Chapter 1
    Section 1.1
    Section 1.2
Chapter 2
    Section 2.1

Advertencia

No todos los formatos de salida admiten un índice con varios niveles. Pruebe primer con el formato de salida EPUB. Si funciona, intente con su formato de elección.

Usar imágenes como títulos de capítulo al convertir documentos de entrada HTML

Supongamos que quiere utilizar una imagen como título de capítulo, pero también desea que calibre pueda generar automáticamente un índice a partir de los títulos de capítulo. Use el siguiente código HTML para lograrlo:

<html>
    <body>
        <h2>Chapter 1</h2>
        <p>chapter 1 text...</p>
        <h2 title="Chapter 2"><img src="chapter2.jpg" /></h2>
        <p>chapter 2 text...</p>
    </body>
</html>

Configure Primer nivel del índice como //h:h2. Entonces, para el capítulo dos, calibre tomará el título del valor del atributo title de la etiqueta <h2>, dado que ésta no posee texto.

Usar atributos de etiquetas para suministrar el texto de las entradas del índice

Si los capítulos tienen títulos especialmente largos y quiere versiones más cortas en el Índice, puede usar el atributo «title» para ello, por ejemplo:

<html>
    <body>
        <h2 title="Chapter 1">Chapter 1: Some very long title</h2>
        <p>chapter 1 text...</p>
        <h2 title="Chapter 2">Chapter 2: Some other very long title</h2>
        <p>chapter 2 text...</p>
    </body>
</html>

Establezca la opción Índice de nivel 1 en //h:h2/@title. Entonces calibre tomará el título a partir del valor del atributo title de las etiquetas <h2>, en lugar de usar el texto dentro de la etiqueta. Fíjese en la terminación /@title de la expresión XPath, puede usar esta forma para indicarle a calibre que tome el texto del atributo que desee.

Cómo se establecen y guardan las opciones de conversión

Existe dos lugares donde en calibre donde se pueden especificar las opciones de conversión. El primer es en :guilabel!`Preferencias > Conversión`. Estos valores son los predeterminados para las opciones de conversión. Cada vez que intente convertir un nuevo libro, las opciones especificadas aquí serán las predeterminadas.

También puede modificar la configuración en el cuadro de diálogo de conversión para cada libro. Cuando convierte un libro, calibre recuerda la configuración que usó para ese libro, de manera que si vuelve a convertirlo, la configuración almacenada tendrá prioridad sobre la predeterminada en las Preferencias. Puede restablecer la configuración individual a la predeterminada usando el botón Restaurar valores predeterminados en el cuadro de diálogo de conversión individual. Puede eliminar las configuraciones almacenadas para un grupo de libros seleccionando los libros y pulsando el botón Modificar metadatos para mostrar el cuadro de diálogo de modificar metadatos en masa, en la parte inferior hay una opción para eliminar las configuraciones de conversión guardadas.

Al convertir en masa un conjunto de libros, las configuraciones se toman en el siguiente orden (el último prevalece):

De la configuración predeterminada en Preferencias > Conversión

A partir de las opciones de conversión guardadas para cada libro que se convierte (si existen). Esto puede desactivarse con la opción en la parte superior izquierda del cuadro de diálogo de conversión en masa.

De la configuración establecida en el cuadro de diálogo Convertir en masa

Tenga en cuenta que las configuraciones finales para cada libro en una conversión en masa se guardarán y se volverán a usar si el libro se convierte de nuevo. Puesto que en una conversión en masa se da máxima prioridad a las configuraciones especificadas en el cuadro de diálogo, éstas reemplazaran cualquier configuración específica de un libro. Las excepciones son los metadatos y las configuraciones específicas de formatos de entrada. El cuadro de diálogo de conversión en masa no tiene configuraciones para estas dos categorías, así que se tomarán de las configuraciones específicas de cada libro (si existen) o de las predeterminadas.

Nota

Puede ver las opciones usadas realmente en la conversión pulsando sobre el icono rotatorio en la esquina inferior derecha y luego pulsando dos veces sobre cada tarea de conversión. Esto mostrará un registro de conversión que contiene las opciones usadas finalmente, en la parte superior.

Consejos para formatos específicos

Aquí encontrará consejos específicos para la conversión de formatos particulares. En el cuadro de diálogo de conversión hay opciones específicas para cada formato, ya sea de entrada o salida, en su propia sección, por ejemplo Entrada TXT o Salida EPUB.

Convertir documentos de Microsoft Word

calibre puede convertir automáticamente archivos .docx creados por Microsoft Word 2007 y versiones posteriores. Sólo tiene que añadir el archivo a calibre y pulsar en Convertir.

Nota

Hay un archivo .docx de demostración [https://calibre-ebook.com/downloads/demos/demo.docx] que muestra las capacidades del motor de conversión de calibre. Descárguelo y conviértalo a EPUB o AZW3 para ver lo que calibre puede hacer.

calibre generará automáticamente un índice basado en las cabeceras si éstas están marcadas con los estilos de Microsoft Word Título 1, Título 2, etc. Abra el libro resultante en el visor de calibre y pulse el botón de Índice para ver el índice generado.

Archivos .doc antiguos

Para los archivos .doc más antiguos, puede guardar el documento como HTML con Microsoft Word y luego convertir el HTML resultante con calibre. Al guardar como HTML, asegúrese de usar la opción «Guardar como página web, filtrada», ya que esto producirá un HTML más limpio que se convertirá mejor. Tenga en cuenta que Word produce un HTML realmente intrincado, y convertirlo puede llevar tiempo, así que sea paciente. Si tiene disponible una versión de Word más reciente, también puede guardarlo directamente como .docx.

Otra posibilidad es utilizar el paquete ofimático libre LibreOffice. Abra el archivo .doc en LibreOffice y guárdelo como .docx., que calibre puede convertir directamente.

Convertir documentos TXT

Los documentos TXT no tienen una manera definida de especificar formato como cursiva, negrita, etc., o estructura del documento com párrafos, cabeceras, secciones y demás, pero existen varias convenciones usadas normalmente. De manera predeterminada calibre intenta detectar automáticamente el formato y marcado correcto basándose en estas convenciones.

La entrada TXT admite una serie de opciones para distinguir cómo se detectan los párrafos.

Estilo de párrafo: auto

Analiza el archivo de texto e intenta determinar automáticamente cómo están definidos los párrafos. Esta opción generalmente funcionará bien, si no obtiene resultados satisfactorios pruebe con las opciones manuales.

Estilo de párrafo: block

Asume que los párrafos están separados por una o más líneas en blanco:

This is the first.

This is the
second paragraph.

Estilo de párrafo: single

Asume que cada línea es un párrafo:

This is the first.
This is the second.
This is the third.

Estilo de párrafo: print

Asume que cada párrafo se inicia con una sangría (ya sea una tabulación o más de un espacio). Los párrafos terminan cuando se alcanza la siguiente línea que empieza con una sangría:

  This is the
first.
  This is the second.

  This is the
third.

Estilo de párrafo: unformatted

Asume que el documento no posee formato, pero usa saltos de línea forzados. La puntuación y la mediana de la longitud de renglón se emplean para intentar restaurr los párrafos.

Estilo de formato: auto

Intenta detectar el tipo de marcado de formato que se emplea. Si no se encuentra uno, se aplicará el formato heurístico.

Estilo de formato: heuristic

Analiza el documento para detectar cabeceras de capítulo comunes, cambios de escena y palabras en cursiva, y aplica las etiquetas HTML adecuadas durante la conversión.

Estilo de formato: markdown

calibre también admite pasar la entrada TXT por un preprocesador llamado Markdown. Markdown permite añadir formato básico a documentos TXT, como negritas, cursivas, encadezados de secciones, tablas, listas, índice, etc. La manera más sencilla de obtener un índice a partir de un documento TXT es marcar las cabeceras de capítulo con «#» y establecer la expresión XPath para detección de capítulos en «//h:h1». Puede aprender más sobre la sintaxis Markdown en daringfireball [https://daringfireball.net/projects/markdown/syntax] (en inglés).

Estilo de formato: none

No aplica ningún formato especial al texto, el documento se convierte a HTML sin ningún otro cambio.

Convertir documentos PDF

Los documentos PDF son uno de los peores orígenes para la conversión. Se trata de un formato con tamaño de página y posición de texto fijos. Esto significa que es muy difícil determinar dónde acaba un párrafo y empieza el siguiente. calibre intentará unir los párrafos usando un Factor de unión de líneas configurable. Esto es una escala usada para determinar la longitud requerida para unir los renglones. Los valores válidos son decimales entre 0 y 1. El valor predeterminado es 0,45, algo por debajo de la longitud de renglón mediana. Reduzca el valor para incluir más texto en la unión, auméntelo para incluir menos. Puede ajustar este valor en las opciones de conversión bajo Entrada PDF.

Además, muchas veces tienen encabezados y pies de página como parte del documento, que aparece incluido con el texto. Use el panel de Buscar y sustituir para eliminar encabezados y pies de página y solucionar este problema. Si los encabezados y pies de página no se eliminan del texto pueden afectar a la unión de párrafos. Para aprender cómo usar las opciones de eliminación de encabezados y pies de página, vea Todo acerca de cómo utilizar expresiones regulares en calibre.

Algunas limitaciones de la entrada de PDF son:

No soporta documentos complejos, con columnas múltiples o basados en imágenes.

Tampoco soporta la extracción de imágenes vectoriales y tablas incluidos en el documento.

Algunos PDF usan glifos especiales para representar «ll», «ff», «fi», etc. La conversión de éstos puede o no funcionar dependiendo de cómo se representant internamente en el PDF.

No soporta enlaces e índices

Los PDF que utilizan tipos de letra incrustados que no son Unicode para representar caracteres no ingleses darán un resultado incorrecto para dichos caracteres

Algunos PDF están hechos de fotografías de la página con el texto resultante del OCR (reconocimiento óptico de caracteres) oculto tras la imagen. En tales casos calibre utiliza el texto del OCR, que puede ser muy diferente de lo que se ve al visualizar el archivo PDF

Los PDF usados para mostrar texto complejo, como idiomas que se leen de derecha a izquierda y expresiones matemáticas, no se convertirán correctamente

Insisto, PDF es un formato muy, muy malo para usarlo como entrada. Si de todas formas tiene que usar PDF, esté preparado para obtener una salida entre decente e inservible, según cómo sea el PDF de entrada.

Colecciones de libros de historietas

Una colección de libros de historietas es un archivo .cbc. Un archivo .cbc es un archivo ZIP que contiene otros archivos CBZ o CBR. Además el archivo .cbc debe contener un archivo de texto llamado comics.txt, codificado en UTF-8. El archivo comics.txt debe contener un listado de los archivos de historieta dentro del archivo .cbc, de la forma nombredearchivo:titulo, como se muestra a continuación:

one.cbz:Chapter One
two.cbz:Chapter Two
three.cbz:Chapter Three

El archivo .cbc contendrá:

comics.txt
one.cbz
two.cbz
three.cbz

calibre convertirá automáticamente este archivo .cbc en un libro electrónico con un índice que apunta a cada entrada en comicx.txt.

EPUB de demostración de formato avanzado

Algunos formatos avanzados para archivos EPUB se muestran en este archivo de demostración [https://calibre-ebook.com/downloads/demos/demo.epub]. Este archivo ha sido creado a partir de HTML codificado manualmente con calibre, y está destinado a servir como plantilla para crear otros EPUB.

El archivo HTML que se usó par crearlo está disponible demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. La configuración usada para crear el EPUB a partir del archivo ZIP es:

ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover

Tenga en cuenta que debido a que este archivo explora el potencial del formato EPUB, la mayor parte del formato avanzado no va a funcionar en lectores menos capacitados que el visor incorporado de calibre.

Convertir documentos ODT

calibre puede convertir directamente archivos ODT (OpenDocument Text). Es recomendable que use estilos para dar formato al documento, manteniendo al mínimo el uso de formato directo. Al insertar imágenes en el documento debe anclarlas al párrafo. Las imágenes ancladas a la página acabarán todas al inicio de la conversión.

Para permitir la detección automática de capítulos, debe marcarlos con los estilos incorporados llamados Encabezado 1, Encabezado 2, …, Encabezado 6 (Encabezado 1 equivale a la etiqueta HTML <h1>, Encabezado 2 a <h2>, etc.). Cuando convierta en calibre puede indicar qué estilo ha usado en el cuadro Detectar capítulos en. Ejemplo:

Si ha marcado los capítulos con estilo Encabezado 2, debe establecer :guilabel`Detectar capítulos en` como //h:h2

Para obtener un índice anidado con las secciones marcadas con Encabezado 2 y los capítulos con Encabezado 3, tendrá que introducir //h:h2|//h:h3. En el apartado Índice del cuadro de diálogo de conversión ponga //h:h2 en Primer nivel del índice y //h:h3 en Segundo nivel del índice.

Las propiedades del documento más comunes (título, palabras clave, descripción, creador) son reconocidas y calibre utilizará la primera imagen (no demasiado pequeña y con proporciones adecuadas) como imagen de portada.

También hay modo avanzado de conversión de propiedades, que se activa estableciendo la propiedad personalizada opf.metadata (de tipo «Sí o no») en Sí en el documento ODT (Archivo > Propiedades > Propiedades personalizadas). Si calibre detecta esta popiedad, se reconocen las siguientes propiedades personalizadas (opf.authors sustituye al creador del documento):

opf.titlesort
opf.authors
opf.authorsort
opf.publisher
opf.pubdate
opf.isbn
opf.language
opf.series
opf.seriesindex

Además de esto, puede especificar la imagen que se usará como portada dándole el nombre opf.cover (pulse con el botón derecho, Imagen > Opciones > Nombre) en el ODT. Si no se encuentra una imagen con este nombre, se usa el método «inteligente». Como la detección de portada puede dar lugar a doble portada en algunos formatos, el proceso eliminará el párrafo (sólo si su único contenido es la imagen) del documento. ¡Pero esto sólo funciona con la imagen con nombre!

Para deshabilitar la detección de portadas puede establecer la propiedad personalizada opf.nocover (de tipo «sí o no») en Yes en el modo avanzado.

Convertir a PDF

La primera y más importante decisión al convertir a PDF es el tamaño de página. De manera predeterminada, calibre usa una página de tamaño «carta norteamericana». Puede cambiarlo a cualquier otro tamaño usual o personalizado en la sección Salida PDF del cuadro de diálogo de conversión. Si va a generar un PDF para usarlo en un dispositivo específico, puede activar la opción para usar el tamaño de página del perfil de salida en su lugar. De esta forma, si el perfil de salida es Kindle, calibre creará un PDF con un tamaño de página adecuado para una pantalla de Kindle.

Encabezados y pies de página

Puede insertar encabezados y pies de página arbitrarios en cada página del PDF especificando plantillas de encabezado y pie de página. Las plantillas son fragmentos de código HTML que aparecen en las ubicaciones de los encabezados y pies de página. Por ejemplo, para mostrar los números de página centrados en la parte inferior de cada página, en verde, utilice la siguiente plantilla:

<footer><div style="margin: auto; color: green">_PAGENUM_</div></footer>

calibre sustituirá automáticamente _PAGENUM_ por el número de página actual. Puede incluso disponer diferente contenido en las páginas pares e impares, por ejemplo, la siguiente plantilla de cabecera mostrará el título en las páginas impares y el autor en las pares:

<header style="justify-content: flex-end">
    <div class="even-page">_AUTHOR_</div>
    <div class="odd-page"><i>_TITLE_</i></div>
</header>

calibre sustituirá automáticamente _TITLE_ y _AUTHOR_ por el título y el autor del documento que se está convirtiendo. Si establece justify-content a flex-end, el texto se alineará a la derecha.

También puede mostrar texto en los bordes izquierdo y derecho y cambiar el tamaño de letra, como se ve en esta plantilla de cabecera:

<header style="justify-content: space-between; font-size: smaller">
    <div>_TITLE_</div>
    <div>_AUTHOR_</div>
</header>

Esto dispondrá el título a la izquierda y el autor a la derecha, en una letra más pequeña que el texto principal.

También puede usar la sección actual en plantillas, como se muestra a continuación:

<header><div>_SECTION_</div></header>

_SECTION_ se sustituye por el nombre que tenga la sección actual. El nombre se extrae del índice de metadatos (la guía PDF). Si el documento no tiene índice, se sustituirá por texto vacío. Si una página PDF tiene varias secciones, se usará la primera de ellas. Igualmente, hay una variable llamada _TOP_LEVEL_SECTION_ que puede usarse para obtener el nombre de la sección actual de nivel superior.

Puede incluso utilizar código JavaScript dentro de las plantillas de encabezado y pie de página, por ejemplo, la siguiente plantilla hará que los números de página comiencen por4 en lugar de 1:

<footer>
    <div></div>
    <script>document.currentScript.parentNode.querySelector("div").innerHTML = "" + (_PAGENUM_ + 3)</script>
</footer>

Además hay otras variables que puede usar en encabezados y pies de página, descritas a continuación:

_TOTAL_PAGES_ - número total de páginas en el archivo PDF, útil para incluir un marcador de progreso, por ejemplo.

_TOP_LEVEL_SECTION_PAGES_ - número total de páginas en la sección de mayor nivel actual

_TOP_LEVEL_SECTION_PAGENUM_ - el número de página de la página actual dentro de la sección de mayor nivel actual

Nota

Si añade encabezados y pies de página, asegúrese de que establece los márgenes superior e inferior con una dimensión suficiente, en la sección Salida del cuadro de diálogo de conversión.

Índice imprimible

También puede incluir un índice imprimible al final del PDF que muestra los números de página de cada sección. Esto es muy útil si va a imprir el PDF en papel. Si va a usar el PDF en un dispositivo electrónico, el Esquema del PDF cumple esta función y se genera de manera predeterminada.

Se puede personalizar el aspecto de los índices generados usando la opción CSS adicional en la sección Apariencia del cuadro de diálogo de conversión. El código CSS usado de manera predeterminada se muestra debajo, cópielo y modifíquelo a su gusto.

.calibre-pdf-toc table { width: 100%% }

.calibre-pdf-toc table tr td:last-of-type { text-align: right }

.calibre-pdf-toc .level-0 {
    font-size: larger;
}

.calibre-pdf-toc .level-1 td:first-of-type { padding-left: 1.4em }
.calibre-pdf-toc .level-2 td:first-of-type { padding-left: 2.8em }

Márgenes de página específicos para archivos HTML individuales

Si está convirtiendo un archivo EPUB o AZW3 que contiene varios archivos HTML y quiere cambiar los márgenes de página de un archivo HTML concreto, puede añadir el siguiente bloque de estilo al archivo HTML usando el editor de libros electrónicos de calibre:

<style>
@page {
    margin-left: 10pt;
    margin-right: 10pt;
    margin-top: 10pt;
    margin-bottom: 10pt;
}
</style>

Después en la sección de salida PDF del cuadro de diálogo de conversión, desactive la opción Usar los márgenes de página del documento de origen. Ahora todas las páginas generadas a partir de este archivo HTML tendrán márgenes de 10pt.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Modificar libros electrónicos

Modificar libros electrónicos

calibre tiene un editor de libros electrónicos integrado que puede usarse para modificar libros en formatos EPUB y AZW3 (Kindle). El editor muestra el código HTML y CSS usado internamente en los archivos del libro, con una vista previa que se actualiza según se hacen cambios. También contiene varias herramientas automatizadas para realizar tareas comunes de limpieza y corrección.

Puede utilizar este editor pulsando con el botón derecho sobre cualquier libro en calibre y seleccionando Modificar libro.

Índice

Flujo de trabajo básico

El explorador de archivos

Cambiar nombres de archivo

Unir archivos

Cambiar el orden de los archivos de texto

Marcar la portada

Borrar archivos

Exportar archivos

Añadir nuevas imágenes, tipos de letra, etc. o crear nuevos archivos en blanco.

Sustituir archivos

Vincular hojas de estilo con archivos HTML de manera eficiente

Buscar y sustituir

Búsquedas guardadas

Modo de función

Buscar omitiendo etiquetas HTML

Herramientas automatizadas

Modificar el índice

Comprobar el libro

Añadir una portada

Incrustar tipos de letra referenciados

Reducir tipos de letra incrustados

Corregir puntuación

Transformar propiedades CSS

Eliminar reglas CSS sin usar

Corregir HTML

Embellecer archivos

Insertar un índice en línea

Establecer conceptos

Filtrar información de estilo

Actualizar el código interno del libro

Hitos

El panel de previsualización en vivo

Dividir archivos HTML

El panel de CSS en vivo

Herramientas variadas

La vista del Índice

Comprobar la ortografía en el libro

Insertar caracteres especiales

La vista del inspector de código

Comprobar enlaces externos

Descargar recursos externos

Organizar archivos en carpetas por tipo

Importar archivos en otros formatos de libro electrónico como EPUB

La herramienta de Informes

Características especiales del editor de código

Resaltado de sintaxis

Ayuda sensible al contexto

Completado automático

Fragmentos

Flujo de trabajo básico

Nota

Un recorrido en forma de video del editor de libros electrónicos de calibre está disponible aquí [https://calibre-ebook.com/es/demo#tutorials].

Al abrir un libro con la herramienta Modificar libro, aparecerá una lista de archivos a la izquierda. Éstos son los archivos HTML individuales, hojas de estilo, imágenes, etc. que constituyen el contenido del libro. Simplemente pulse dos veces en un archivo para comenzar a modificarlo. Tenga en cuenta que si quiere hacer alg más sofisticado que unos pequeños retoques, debe conocer HTML (cursillo) [http://html.net/tutorials/html/] y CSS (cursillo) [http://html.net/tutorials/css/].

Según se hacen cambios en el código HTML o CSS, éstos se muestran en vivo en el panel de vista previa a la derecha. Cuando esté satisfecho con el aspecto de los cambios, pulse en el botón Guardar o use Archivo > Guardar para guardar los cambios en el libro.

Una característica útil es Hitos. Antes de comenzar un conjunto ambicioso de modificaciones, puede crear un hito. El hito preservará el estado actual del libro y, si en el futuro decide que no quiere conservar los cambios realizados, podrá volver a la situación de cuando se creó el hito. Para crear un hito, use Editar > Crear hito. También se crean hitos automáticamente cada vez que ejecute una herramienta automatizada como una búsqueda y sustitución global. Los hitos son una adición al mecanismo normal de deshacer y rehacer al modificar archivos individuales, y son necesarios cuando los cambios afectan a varios archivos en el libro.

Ésta es la secuencia de trabajo básica para modificar libros: Abrir un archivo, hacer los cambios, comprobar la vista previa y guardar. En el resto de este manual hablaremos de las herramientas y características que le permitirán realizar tareas especificas de manera eficiente.

El explorador de archivos

El Explorador de archivos proporciona un resumen de los distintos archivos dentro del libro que está modificando. Los archivos están dispuestos por categoría, con los archivos de texto (HTML) al principio, seguidos de los archivos de estilo (CSS), imágenes, etc. Pulse dos veces sobre un archivo para modificarlo. Puede modificar archivos HTML, CSS y de imagen. El order de los archivos de texto es el mismo orden en que mostrarían si estuviera leyendo el libro. El resto de archivos están ordenados alfabéticamente.

Pasando el ratón por encima de una entrada, puede ver su tamaño y, en la parte inferior de la pantalla, la ruta de acceso completa al archivo dentro del libro. Tenga en cuenta que los archivos en el libro están comprimidos, por lo que el tamaño final del libro no es la suma de los tamaños de los archivos individuales.

Muchos archivos tienen un significado especial en el libro. Normalmente éstos tendrán un icono junto a su nombre, indicando el significado especial. Por ejemplo, en la imagen de la izquierda puede ver que los archivos cover_image.jpg y titlepage.xhtml tienen el icono de una portada, lo que indica que son la imagen de portada y la página de título. Igualmente, el archivo content.opf tiene un icono de metadatos, indicando que contiene los metadatos del libro, y el archivo toc.ncx tiene un icono de T, indicando que se trata del índice.

Puede realizar varias acciones sobre archivos individuales, pulsando con el botón derecho sobre ellos.

Cambiar nombres de archivo

Puede cambiar el nombre de un archivo concreto pulsando con el botón derecho sobre él y seleccionando Cambiar nombre. Al cambiar el nombre de un archivo se actualizan automáticamente todos los enlaces y referencias al archivo en todo el libro. Todo lo que tiene que hacer es escribir un nuevo nombre y calibre se encarga del resto.

También puede cambiar el nombre de muchos archivos a la vez. Esto es útil si quiere que los nombres de los archivos sigan un patrón sencillo. Por ejemplo, puede querer cambiar el nombre de todos los archivos HTML para que sean Capitulo-1.html, Capitulo-2.html, etc. Seleccione los archivos a los que quiere cambiar el nombre manteniendo pulsada la tecla Mayús o Ctrl y pulsando con el ratón sobre los archivos. Luego pulse con el botón derecho y seleccione Cambiar el nombre en masa. Introduzca un prefijo y el número por el que desea que empiece la numeración automática, pulse «Aceptar» y ya está. El diálogo para cambiar el nombre en masa también le permite cambiar el nombre de los archivos según el orden en que aparecen en el libro, en lugar del orden en que se seleccionaron, por ejemplo para numerar todas las imágenes en orden de aparición.

Por último, puede cambiar la extensión de todos los archivos seleccionados. Seleccione varios archivos, como antes, pulse con el botón derecho y elija Cambiar la extensión de los archivos seleccionados.

Unir archivos

A veces, puede querer unir dos archivos HTML o dos archivos CSS. En ciertas ocasiones puede ser útil tener todo en un solo archivo. Pero tenga cuidado, poner una gran cantidad de contenido en un solo archivo causará problemas de rendimiento cuando visualice su libro en un lector de libros electrónicos normal.

Para combinar varios archivos en uno, selecciónelos manteniendo pulsada la tecla Ctrl y pulsando con el ratón sobre ellos (asegúrese de que sólo selecciona archivos de un tipo, ya sean todos HTML o todos CSS). Luego pulse con el botón derecho y seleccione Unir. Eso es todo, calibre combinará los archivos, migrando automáticamente todos los enlaces y referencias a los archivos originales. Tenga en cuenta que al combinar archivos se puede dar lugar a cambios de estilo, pues los archivos individuales podían usar diferentes hojas de estilo.

También puede seleccionar archivos de texto y arrastrarlos sobre otro archivo de texto para unirlos a él.

Cambiar el orden de los archivos de texto

Puede modificar el orden en el que se abren los archivos de texto (HTML) al leer el libro arrastrándolos en el :guliabel:`Explorador de archivos` o pulsando sobre el archivo a mover y luego los modificadores Ctrl+Mayús con las teclas Arriba, Abajo, Inicio o Fin. Para quien tenga inclinaciones técnicas, esto es lo que se llama reordenar el lomo.

Tenga en cuenta que al arrastrar los elementos debe soltaros entre otros elementos, no sobre ellos, lo que puede ser un poco delicado hasta que se acostumbre. Si suelta un archivo sobre otro archivo, los dos archivos se fusionarán.

Marcar la portada

Los libros electrónicos tienen normalmente una imagen de portada. Esta imagen se indica en Explorador de archivos con un icono de un libro marrón junto al nombre de la imagen. Si quiere designar otra imagen como la portada, puede hacerlo pulsando con el botón derecho sobre el archivo y eligiendo Marcar como imagen de portada.

Además, los archivos EPUB tienen el concepto de página de título. Una página de título es un archivo HTML que funciona como la portada o portadilla del libro. Puede marcar un archivo HTML como página de título al modificar archivos EPUB pulsando con el botón derecho. Asegúrese de que el archivo marcado contiene sólo la información de portada. Si tiene otro contenido, como el primer capítulo, dicho contenido se perderá si se convierte el libro a algún otro formato con calibre. Esto sucede porque, durante la conversión, calibre asume que la página de título contiene sólo la portada y nada más.

Borrar archivos

Puede borrar archivos pulsando con el botón derecho sobre ellos o seleccionándolos y pulsando la tecla Supr. Al borrar un archivo se eliminan todas las referencias a éste en el archivo OPF, evitándole la molestia. Sin embargo, las referencias en otros lugares no se eliminan. Puede usar la herramienta Comprobar libro para encontrarlas y eliminarlas o sustituirlas.

Exportar archivos

Puede exportar un archivo contenido en el libro a algún otro lugar del equipo. Esto es útil si quiere trabajar en el archivo de manera aislada, con herramientas especializadas. Para ello, pulse con el botón derecho sobre el archivo y elija Exportar

Una vez que haya terminado con el archivo exportado, puede volver a importarlo en el libro, pulsando con el botón derecho sobre el archivo otra tez y eligiendo Sustituir por archivo…, lo que le permitirá sustituir el archivo del libro por el archivo previamente exportado.

También puede copiar archivos entre distintas sesiones del editor. Seleccione los archivos que desee copiar en el Explorador de archivos, después pulse con el botón derecho y elija Copiar los archivos seleccionados a otra sesión del editor. Luego, en la otra sesión del editor, pulse con el botón derecho en el Explorador de archivos y elija la opción Pegar archivo desde otra sesión del editor.

Añadir nuevas imágenes, tipos de letra, etc. o crear nuevos archivos en blanco.

Puede añadir una nueva image, tipo de letra, hoja de estilo, etc. del equipo al libro, seleccionando Archivo > Nuevo archivo. Esto le permite importar un archivo pulsando el botón :guilabel:`Importar archivo de recursos`o crear un archivo HTML u hoja de estilo en blanco, introduciendo su nombre en el cuadro.

También puede importar múltiples archivos en el libro de una vez usando Archivo > Importar archivos a libro.

Sustituir archivos

Puede sustituir archivos existentes en el libro pulsando con el botón derecho y eligiendo Sustituir. Esto actualizará automáticamente todos los enlaces y referencias en el caso de que el nuevo archivo tenga un nombre distinto del original.

Vincular hojas de estilo con archivos HTML de manera eficiente

Como opción práctica, puede seleccionar varios archivos HTML en el Explorador de archivos, pulsar con el botón derecho y elegir Enlazar hojas de estilo, para que calibre inserte automáticamente las etiquetas <link> de las hojas de estilo en todos los archivos HTML seleccionados.

Buscar y sustituir

El editor de libros tiene una interfaz muy potente de búsqueda y sustitución que le permite buscar y sustituir texto en el archivo actual, en todos los archivos e incluso en una región marcada del archivo actual. Puede buscar usando una búsqueda normal o expresiones regulares. Para aprender cómo usar las expresiones regulares en búsquedas avanzadas, vea Todo acerca de cómo utilizar expresiones regulares en calibre.

Inicie la búsqueda y sustitución en el menú Buscar > Buscar y sustituir (debe estar modificando un archivo HTML o CSS).

Introduzca el texto que quiere encontrar en el cuadro Buscar y lo que quiere poner en su lugar en el cuadro Sustituir. Puede pulsar los botones respectivos para buscar la siguiente coincidencia, sustituir la coincidencia actual y sustituir todas las coincidencias.

Mediante los cuadros desplegables en la parte inferior puede hacer que la búsqueda se realice sobre el archivo actual, todos los archivos de texto, todos los archivos de estilo o todos los archivos. También puede elegir el modo de búsqueda entre normal (texto) o expresión regular.

Puede contar todas las coincidencias de una expresión de búsqueda mediante de Buscar > Contar todo. El recuento se ejecutará sobre los archivos o regiones seleccionados en en el menú desplegable.

También puede ir a una línea específica en el editor abierto actualmente por medio de Buscar > Ir a línea.

Nota

Recuerde que para poder aprovechar toda la potencia de buscar y sustituir tendrá que usar expresiones regulares. Véase Todo acerca de cómo utilizar expresiones regulares en calibre.

Búsquedas guardadas

Puede guardar expresiones frecuentes de búsqueda y sustitución (incluidas expresiones de modo de función) para volver a usarlas en otras ocasiones. Para guardar una búsqueda no tiene más que pulsar con el botón derecho en el cuadro de Buscar y seleccionar Guardar búsqueda actual.

Puede abrir las búsquedas guardadas mediante :guilabel: Buscar > Búsquedas guardadas. Esto le mostrará una lista de expresiones de búsqueda y sustitución que se pueden aplicar. Incluso puede seleccionar varias entradas de la lista, manteniendo pulsada la tecla Ctrl mientras pulsa con el ratón, con el fin de ejecutar múltiples expresiones de búsqueda y sustitución en una sola operación.

Modo de función

El modo de función le permite escribir funciones Python tan potentes como desee, que se ejecutan en cada búsqueda o sustitución. Puede realizar prácticamente cualquier manipulación de texto en el modo de función. Para más información vea Modo de función para buscar y sustituir en el editor.

Buscar omitiendo etiquetas HTML

También hay una herramienta de búsqueda específica para buscar texto, omitiendo las etiquetas HTML intermedias. Por ejemplo, si el libro contiene el HTML «Una <i>palabra</i> en cursiva», puede buscar «una palabra» y encontrar el texto, aunque hay una etiqueta «<i>» en el medio. Esta herramienta se encuentra en el menú Búsqueda > Buscar omitiendo HTML.

Herramientas automatizadas

Modificar libro posee varias herramientas útiles para tareas usuales. A éstas se accede a través del menú Herramientas.

Modificar el índice

Hay una herramienta específica para facilitar la edición del Índice. Iníciela con Herramientas > Índice > Modificar índice.

La herramienta Modificar índice le muestra el índice actual (si existe) a la izquierda. Pulse dos veces en cualquier entrada para modificar su texto. Puede también reorganizar las entradas arrastrando y soltando o utilizando los botones de la derecha.

Para los libros que no tienen un índice, la herramienta le ofrece varias opciones para generar uno automáticamente a partir del texto. Puede generarlo a partir de los títulos del documento, de los enlaces, de los archivos individuales, etc.

Puede modificar las entradas individuales pulsando sobre ellas y después sobre el botón Cambiar la ubicación a la que apunta esta entrada. Esto abrirá una vista previa del libro. Mueva el cursor al panel de vista del libro y pulse en el lugar donde quiera que apunte la entrada. Una línea gruesa verde mostrará la ubicación. Pulse Aceptar cuando esté satisfecho con la ubicación.

Comprobar el libro

La herramienta Comprobar libro busca posibles problemas en el libro que pueden hacer que no funcione como se pretende en dispositivos de lectura reales. Actívela en Herramientas > Comprobar libro.

Cualquier problema que se encuentre se mostrará en una lista clara y fácil de usar. Al pulsar sobre cualquier entrada de la lista se mostrará más información sobre el error y se le dará la opción de corregirlo automáticamente si es posible. También puede pulsar dos veces sobre un error para abrir la ubicación del error en un editor, para que pueda corregirlo a mano.

Algunas de las comprobaciones realizadas son:

Código HTML defectuoso. Todo código HTML que no puede procesarse como XML correcto se muestra en un informe. Al corregirlo se asegurará de que funciona como se pretende en todos los contextos. calibre puede también corregir automáticamente estos errores, pero la corrección automática puede tener efectos inesperados en ocasiones, así que úsela con precaución. Como siempre, se creará un hito antes de la corrección automática, por lo que es fácil deshacer los cambios. La corrección automática funciona procesando el código con el algoritmo HTML5, que tolera muchos errores, y convirtiéndolo luego a XML correcto.

Estilos CSS defectuosos o desconocidos. Cualquier CSS que no sea válido o que tenga propiedades no definidas según el estándar CSS 2.1 (más algunas de CSS 3) aparece en el informe. Se comprueba el código CSS de todas las hojas de estilo, atributos de style en línea y las etiquetas <style> en los archivos HTML.

Enlaces rotos. Se informa de los enlaces que apuntan a archivos en el libro que no existen.

Archivos sin referencias. Se muestran los archivos en el libro que no están referenciados por ningún otro archivo o no están en el lomo.

Varios problemas comunes en archivos OPF tales como elementos del manifiesto o lomo duplicados, idrefs o etiquetas meta de portadas rotos, secciones requeridas ausentes, etc.

Se ralizan distintas comprobaciones de compatibilidad relativas a problemas conocidos que pueden provocar un mal funcionamiento del libro electrónico en dispositivos de lectura.

Añadir una portada

Puede añadir fácilmente una portada al libro por medio de Herramientas > Añadir portada. Esto le permite elegir una imagen existente en el libro como la portada o importar una nueva imagen al libro y hace que sea la portada. Al modificar archivos EPUB, el contenedor HTML para la portada se genera de forma automática. Si se encuentra una portada existente en el libro, se sustituye. La herramienta también se encarga automáticamente de marcar correctamente los archivos de la portada como portadas en el OPF.

Incrustar tipos de letra referenciados

Se accede a través de Herramientas > Incrustar tipos de letra referenciados, esta herramienta busca todos los tipos de letra referenciados en el libro y si no están ya incrustados, los busca en el equipo y los incrusta en el el libro, si los encuentra. Haga el favor de asegurarse de que tiene los permisos necesarios para incrustar tipos de letra con licencia comercial antes de hacerlo.

Reducir tipos de letra incrustados

Se accede a través de Herramientas > Reducir tipos de letra, esta herramienta reduce todos los tipos de letra incrustados en el libro para que contengan sólo los glifos necesarios para el texto realmente presente en el libro. Normalmente esto reduce el tamaño de los archivos de tipo de letra en alrededor del 50%. Sin embargo, tenga en cuenta que una vez reducidos los tipos de letra, si añade nuevo texto cuyos caracteres no estaban previamente presentes en el tipo de letra reducido, el tipo de letra no funcionará para el nuevo texto. Así que realice esta acción solamente en el último paso del trabajo.

Corregir puntuación

Convertir guiones sencillos, puntos suspensivos, comillas, guiones múltiples, etc. en sus equivalentes tipográficos correctos. Tenga en cuenta que el algoritmo a veces puede generar resultados incorrectos, especialmente cuando se encuentran comillas simples al inicio de las contracciones. Se accede a través de :guilabel: Herramientas > Mejorar puntuación.

Transformar propiedades CSS

Crear reglas para transformar el estilo del libro. Por ejemplo, crear una regla que convierta todo el texto rojo a verde, o duplicar el tamaño de letra de todo el texto del libro, o poner en cursiva todo el texto en determinado tipo de letra, etc.

Crear las reglas es sencillo, las reglas tienen un formato de idioma natural, de esta manera:

Si la propiedad color es red cambiar a verde

Si la propiedad font-size es cualquier valor multiplicar el valor por 2

Se accede a través de Herramientas > Transformar estilos.

Eliminar reglas CSS sin usar

Eliminar todas las reglas sin usar de las hojas de estilo y etiquetas <style>. Algunos libros creados a partir de plantillas de producción pueden contener un gran número de reglas CSS innecesarias que no afectan a ningún contenido del libro. Estas reglas adicionales pueden ralentizar los lectores, que necesitan procesarlas todas. Accesible en Herramientas > Eliminar CSS sin usar.

Corregir HTML

Esta herramienta no hace más que convertir un HTML que no puede procesarse como XML a XML correcto. Es muy frecuente que los libros electrónicos contengan XML defectuoso, por lo que que esta herramienta automatiza la corrección de dicho código. La herramienta funciona procesando el HTML con el algoritmo HTML5 (el algoritmo usado en todos los navegadores modernos) y convirtiendo el resultado en XML. Tenga en cuenta que la corrección automática puede tener a veces resultados contraintuitivos. Si lo prefiere, puede usar la herramienta :guilable:`Comprobar libro`, discutida anteriormente, para buscar y corregir manualmente problemas en el HTML. Se activa en Herramientas > Corregir HTML.

Embellecer archivos

Esta herramienta se utiliza para dar formato automático a todos los archivos HTML y CSS para que «se vean atractivos». El código se sangra automáticamente para que esté bien alineado, se insertan líneas en blanco donde sea apropiado, etc. Tenga en cuenta que este embellecimiento también corrige automáticamente código HTML o CSS incorrecto. Por lo tanto, si no quiere que se lleve a cabo ninguna corrección automática, utilice primero la herramienta Comprobar libro para corregir todos los problemas y sólo entonces ejecute esta acción. Se accede a través de Herramientas > Embellecer todos los archivos.

Nota

En HTML, cualquier texto puede tener espacios significativos por medio de la directriz CSS «white-space». Por lo tanto, el embellecimiento puede alterar potencialmente cómo se muestra el HTML. Para evitarlo tanto como sea posible, el algoritmo de embellecimiento sólo modifica etiquetas de bloque que contienen otras etiquetas de bloque. Por ejemplo, el texto dentro de una etiqueta <p> no verá sus espacios alterados. Pero una etiqueta <body> que contiene sólo etiquetas <p> y <div> será embellecida. Esto significa que a veces un archivo concreto no se verá afectado por el embellecimiento al no tener etiquetas de bloque que cumplan las condiciones. En tales casos puede probar otras herramientas de embellecimiento que toman menos precauciones, como: HTML Tidy [https://infohound.net/tidy/].

Insertar un índice en línea

Normalmente en los libros electrónicos, el índice es independiente del texto principal y por lo general se accede a través de un botón o menú de índice en el dispositivo de lectura. También puede hacer que calibre genere automáticamente un índice que sea parte del texto del libro. Éste se genera de acuerdo al índice actualmente ya definido.

Si utiliza esta herramienta varias veces, en cada ocasión se sustituirá el Índice creado previamente. La herramienta está accesible en Herramientas > Índice > Insertar un Índice explícito.

Establecer conceptos

Esta herramienta se usa para establecer conceptos en archivos EPUB. Los conceptos son simplemente enlaces en el archivo OPF que identifican ciertas ubicaciones en el archivo con ciertos significados. Puede usarlos para identificar el prefacio, dedicatoria, portada, índice, etc. Elija el tipo de concepto que quiere especificar y luego seleccione la ubicación del libro adonde debe apuntar el enlace. A esta herramienta se accede a través de Herramientas > Establecer concepto.

Filtrar información de estilo

Esta herramienta puede usarse para eliminar determinadas propiedades de estilo CSS de todo el libro. Puede indicarle qué propiedades quiere eliminar, por ejemplo: color, background-color, line-height, y las eliminará de cualquier lugar donde apararezcan: hojas de estilo, etiquetas <style> y atributos style. Una vez eliminada la información de estilo se mostrará un resumen de todos los cambios realizados, para que pueda ver qué ha cambiado exactamente. Se puede acceder a la herramienta en Herramientas > Filtrar información de estilo.

Actualizar el código interno del libro

Esta herramienta puede usarse para actualizar el código interno del libro, si es posible. Por ejemplo, actualizará libros EPUB 2 a EPUB 3. Puede accederse a esta herramienta en Actualizar código del libro.

Hitos

Los Hitos son una forma de marcar el estado actual del libro como «especial». Después hacer todos los cambios que quiera en el libro, y si no está satisfecho con el resultado, puede volver al estado marcado. Los hitos se crean automáticamente cada vez que ejecuta alguna de las herramientas automáticas descritas en la sección anterior.

Puede crear un hito en Editar > Crear hito. Y volver a un hito anterior con Editar > Revertir a…

El uso de hitos es adicional al mecanismo normal de deshacer y rehacer al modificar archivos individuales. Los hitos son necesarios cuando los cambios se extienden por varios archivos del libro o cuando quiere poder revertir un grupo de modificaciones de una vez.

Puede ver una lista de los hitos disponibles en Ver > Hitos. Puede comparar el estado actual del libro con un hito específico con la herramienta Comparar libros electrónicos, seleccionando el hito de interés y pulsando el botón Comparar. El botón Revertir a restaura el libro al hito seleccionado, deshaciendo todos los cambios desde que se creó el hito.

El panel de previsualización en vivo

La Vista previa de archivo le proporciona un resumen de los archivos contenidos. El panel de vista previa en vivo le muestra en vivo los cambios realizados (con uno o dos segundos de retraso). Según modifique los archivos HTML o CSS, el panel de vista previa se actualizará automáticamente para reflejar los cambios. Cuando mueva el cursor en el editor, el panel de vista previa seguirá su ubicación, mostrando la posición correspondiente en el libro. Al pulsar sobre el panel de vista previa, el cursor en el editor se colocará sobre el elemento que haya pulsado. Si pulsa en un enlace que apunte a otro archivo del libro se abrirá automáticamente ese archivo en el editor y el panel de vista previa.

Puede desactivar la sincronización automática de la posición y vista previa en vivo de los cambios, por medio de los botones bajo el panel de vista previa. La actualización en vivo del panel de vista previa sólo ocurre cuando no se está escribiendo activamente en el editor, a fin de no crear una distracción o reducir la velocidad mientras se espera a que se genere la vista previa.

El panel de vista previa muestra el aspecto que tendrá el texto al ser visualizado. No obstante, el panel de vista previa no puede sustituir una comprbación en un dispositivo de lectura real. Es, a la vez, más y menos potente que un dispositivo real. Admitirá errores y código incorrecto mucho más fácilmente que la mayoría de los dispositivos de lectura. Por otro lado, no mostrará márgenes o saltos de página, o tipos de letra incrustados que usen nombres de sustitución. Use el panel de vista previa mientras trabaja en el libro, pero cuando haya concluido revíselo en un dispositive de lectura real o en un emulador.

Nota

El panel de vista previa no admite fuentes incrustadas si el nombre de la fuente dentro del código del archivo no coincide con el nombre de la regla CSS @font-face. Puede utilizar la herramienta Comprobar libro para encontrar y corregir rápidamente cualquier fuente con dichos problemas.

Dividir archivos HTML

Un uso quizá no evidente del panel de vista previa es dividir archivos HTML largos. Mientras visualiza el archivo que quiere dividir, pulse el botón modo de división, bajo el panel de vista previa botón de modo de división. Luego mueva el ratón al lugar donde quiere dividir el archivo y pulse. Aparecerá una línea verde gruesa en el lugar exacto donde ocurrirá la división según mueva el ratón. Una vez que haya encontrado la ubicación que desee, pulse y se efectuará la división.

Al dividir automáticamente el archivo se actualizarán todos los enlaces y referencias que apuntaban a la segunda porción del archivo y se abrirá el archivo recién dividido en un editor.

También puede dividir un único archivo HTML en múltiples ubicaciones de manera automática, pulsando con el botón derecho en el editor y eligiendo Dividir en múltiples ubicaciones. Esto le permite dividir un archvo grande en todas las etiquetas de encabezado, o todas las etiquetas que tienen una determinada clase, o alguna otra condición.

El panel de CSS en vivo

El panel CSS en vivo muestra todas las reglas de estilo que se aplican a la etiqueta que está editando actualmente. Se muestra el nombre de la etiqueta, junto con su número de línea, seguido de una lista de reglas de estilo coincidentes.

Es una manera excelente de ver rápidamente qué reglas de estilo se aplican a cualquier etiqueta. La vista también posee enlaces activos (en azul), que llevan directamente a la ubicación donde se define el estilo, en caso de que desee realizar algún cambio en las reglas de estilo. Se muestran las reglas de estilo que se aplican directamente a la etiqueta, así como las reglas que se heredan de las etiquetas superiores.

El panel también muestra los estilos calculados finales para la etiqueta. Las propiedades en la lista que son reemplazadas por reglas de mayor prioridad se muestran tachadas.

Puede habilitar el panel de CSS en vivo en Ver > CSS en vivo.

Herramientas variadas

Existen algunas herramientas más que pueden ser útiles mientras se modifica un libro.

La vista del Índice

La vista de Índice le muestra el índice actual del libro. Pulsando dos veces sobre una entrada se abre la ubicación a la que apunta la entrada en un editor. Puede pulsar con el botón derecho para modificar el Índice, refrescar la vista o expandir o contraer todos los elementos. Acceda a esta vista en Vista  →  Índice.

Comprobar la ortografía en el libro

Puede ejecutar un corrector ortográfico en Herramientas > Comprobar ortografía.

Las palabras se muestran con el número de veces que aparecen en el libro y el idioma al que pertenecen. La información del idioma se extrae de los metadatos del libro y de los atributos lang en los archivos HTML. Esto permite que la comprobación de ortografía funcione incluso con libros que contienen texto en varios idiomas. Por ejemplo, en el siguiente fragmento HTML la palabra «color» se comprobará como inglés americano, y «colour» como inglés británico.

<div lang="en_US">color <span lang="en_GB">colour</span></div>

Nota

Puede pulsar dos veces sobre una palabra para resaltar la siguiente aparición de esta palabra en el editor. Esto es útil si desea modificar la palabra manualmente, o ver en qué contexto se encuentra.

Para cambiar una palabra, simplemente pulse dos veces en una de las alternativas sugeridas a la derecha, o escriba su propia sugerencia correcta y pulse en el botón Cambiar la palabra seleccionada a. Esto sustituirá todas las apariciones de esta palabra en el libro. También puede pulsar con el botón derecho sobre una palabra en la lista principal de palabras para cambiar la palabra directamente desde el menú emergente.

Puede hacer que la comprobación de ortografía ignore una palabra durante la sesión actual pulsando en el botón Ignorar. También puede añadir una palabra al diccionario de usuario pulsando en el botón Añadir palabra al diccionario. La comprobación de ortografía permite usar varos diccionarios de usuario, por lo que puede elegir a qué diccionario quiere añadir la palabra.

También puede hacer que el corrector ortográfico muestre todas las palabras del libro, no sólo las escritas incorrectamente. Esto es útil para ver qué palabras son las más frecuentes en el libro y ejecutar una simple búsqueda y sustitución de ciertas palabras.

Nota

Si realiza cambios en el libro modificando los archivos mientras la herramienta de corrección ortográfica está abierta, debe pulsar en el botón Actualizar en la herramienta de corrección ortográfica. Si no lo hace y continúa utilizando la herramienta de corrección ortográfica, podría perder los cambios realizados en el editor.

Nota

Para excluir un archivo individual de la revisión ortográfica al ejecutar la herramienta, puede usar el botón Excluir archivos o añadir el siguiente comentario justo debajo de la etiqueta de apertura en el archivo:

<!-- calibre-no-spell-check -->

Añadir nuevos diccionarios

El corrector ortográfico viene con diccionarios predefinidos para los idiomas inglés y español. Puede instalar sus propios diccionarios mediante Preferencias > Editor > Administrar diccionarios de ortografía. El corrector ortográfico puede utilizar diccionarios del programa LibreOffice (con el formato .oxt). Puede descargar estos diccionarios desde el repositorio de extensiones de LibreOffice [https://extensions.libreoffice.org/?Tags%5B%5D=50].

Insertar caracteres especiales

Puede insertar caracteres que son difíciles de teclear mediante la herramienta :guilabel: Edición > Insertar carácter especial. Esto muestra todos los caracteres Unicode, simplemente pulse en el carácter que desea escribir. Si mantiene pulsada la tecla Ctrl mientras pulsa, la ventana se cerrará después de insertar el carácter seleccionado. Esta herramienta se puede utilizar para insertar caracteres especiales en el texto principal o en cualquier otra área de la interfaz de usuario, como la herramienta de búsqueda y sustitución.

Debido a que hay una gran cantidad de caracteres, puede definir sus propios caracteres Favoritos, que se mostrarán en primer lugar. Pulse con el botón derecho sobre un carácter para marcarlo como favorito. También puede pulsar con el botón derecho sobre un carácter en los favoritos para quitarlo de la lista. Por último, se puede volver modificar el orden de los caracteres favoritos pulsando en el botón Reordenar los favoritos y luego arrastrando y soltando los caracteres en favoritos.

También puede escribir directamente caracteres especiales mediante el teclado. Para ello, escriba el código Unicode para el carácter (en hexadecimal) y luego presione Alt+X, lo que convertirá el código previamente escrito en el carácter correspondiente. Por ejemplo, para escribir «ÿ» debería escribir «ff» y luego pulsar Alt+X. Para escribir un espacio duro debería usar «a0» y luego Alt+X, para escribir puntos suspensivos horizontales debería usar «2026» y Alt+X, y así sucesivamente.

Por último, puede escribir caracteres especiales mediante el uso de las entidades con nombr HTML. Por ejemplo, si escribe «&nbsp;» sustituido por un espacio duro al introducir el punto y coma. La sustitución ocurre sólo cuando se escribe el punto y coma.

La vista del inspector de código

Esta vista muestra el código HTML y CSS que se aplica al actual elemento de interés. Se abre pulsando con el botón derecho sobre una ubicación en el panel de vista previa y seleccionando Inspeccionar. Le permite ver el código HTML para ese elemento y lo más importante, los estilos CSS que se aplican al mismo. Incluso puede editar dinámicamente los estilos y ver qué efecto tienen los cambios al instante. Tenga en cuenta que la edición de los estilos en realidad no realiza cambios en el contenido del libro, sólo permite una experimentación rápida. La capacidad de modificación en tiempo real del inspector está en desarrollo.

Comprobar enlaces externos

Puede usar esta herramienta para comprobar todos los enlaces del libro que apuntan a sitios externos. La herramienta intentará visitar todos los enlaces y, si falla, informará de todos los enlaces rotos de manera que sea sencillo corregirlos.

Descargar recursos externos

Puede utilizar esta herramienta para descargar automáticamente cualquier imagen, hoja de estilo, etc. del libro que no esté incluida en él (es decir, que tenga un URL referido a una ubicación en Internet). La herramienta encontrará todos estos recursos, los descargará automáticamente, los añadirá al libro y sustituirá todas las referencias para que se utilicen los archivos descargados.

Organizar archivos en carpetas por tipo

A menudo, al modificar archivos EPUB obtenidos de alguna parte, verá que los archivos dentro del EPUB están organizados arbitrariamente en diferentes subcarpetas. Esta herramienta le permite mover automáticamente todos los archivos a carpetas según su tipo. Se accede en Herramientas > Disponer en carpetas. Tenga en cuenta que esta herramienta sólo cambia la organización de los archivos dentro del EPUB, no cambia la forma en que se muestran en el Explorador de archivos.

Importar archivos en otros formatos de libro electrónico como EPUB

El editor incluye la habilidad de importar archivos en otros formatos de libro electrónico directamente como un nuevo EPUB, sin requerir una conversión total. Esto es particularmente útil para crear archivos EPUB a partir de archivos HTML editados a mano. Puede hacer esto por medio de Archivo > Importar un archivo HTML o DOCX como un libro nuevo.

La herramienta de Informes

El editor incluye una útil herramienta de Informes (en Herramientas > Informes) que muestra resúmenes de los archivos, imágenes, enlaces, palabras, caracteres y estilos usados en el libro. Cada línea del informe es activa. Al pulsar dos veces en una línea se salta al lugar del libro donde se usa o define (según sea más apropiado) el elemento. Por ejemplo, en la vista de Enlaces, puede pulsar dos veces en la columna Origen para saltar al lugar donde se define el enlace y en las entradas de la columna Destino para saltar a donde apunta el enlace.

Características especiales del editor de código

El editor de HTML de calibre es muy potente. Tiene muchas funciones que facilitan la modificación de HTML (y CSS).

Resaltado de sintaxis

El editor HTML tiene un resaltado de sintaxis muy sofisticado. Sus características incluyen:

El texto dentro de etiquetas de negrita, cursiva o cabeceras se muestra en negrita o cursiva

Al mover el cursor por el código HTML, las etiquetas HTML emparejadas se destacan y puede ir directamente a la etiqueta de apertura o cierre con los atajos de teclado Ctrl+{ y Ctrl+}. De manera similar, puede seleccionar el contenido de una etiqueta con Ctrl+Alt+T o Ctrl+Mayús+T.

El código HTML no válido se subraya en rojo

Los problemas de ortografía en el texto dentro de las etiquetas HTML y en atributos como «title» se resaltan. La comprobación de ortografía detecta el idioma especificado en el atributo lang de la etiqueta actual y el idioma global del libro.

El código CSS dentro de etiquetas <style> también se resalta

Los caracteres especiales que pueden ser indistinguibles, como espacios duros, diferentes tipos de guiones y rayas, etc., se resaltan

Los enlaces a otros archivos en etiquetas <a>, <img> y <link> tienen el nombre de archivo resaltado. Si el archivo al que apuntan no existe, el nombre se subraya en rojo.

Ayuda sensible al contexto

Puede pulsar con el botón derecho sobre cualquier nombre de etiqueta o de propiedad CSS para obtener ayuda para esa etiqueta o propiedad.

También puede mantener pulsada la tecla Ctrl y pulsar sobre cualquier nombre de archivo en una etiqueta de enlace para abrir dicho archivo automáticamente en el editor. Análogamente, si pulsa en un nombre de clase con Ctrl irá a la primera regla de estilo que coincida con la etiqueta y la clase.

Si pulsa con el botón derecho sobre un nombre de clase en un archivo HTML podrá cambiar el nombre de la clase, cambiando todas las apariciones de la clase en el libro y sus hojas de estilo.

Completado automático

Al modificar un libro, una de las tareas más engorrosas es crear enlaces a otros archivos dentro del libro, a hojas de estilo CSS o a imágenes. Debe encontrar el nombre y ubicación relativa correctos para el archivo. El editor tiene una función de completado automático para hacerlo más sencillo.

Según teclea un nombr de archivo, el editor muestra sugerencias automáticas. Use la tecla Tab para seleccionar el nombre correcto. El editor también ofrece sugerencias para enlaces que apuntan a un punto de anclaje dentre de otro archivo HTML. Después de introducir el carácter #, el editor mostrará una lista de todos los puntos de anclaje en el archivo de destino, con un pequeño fragmento de texto para ayudarle a elegir el punto de anclaje correcto.

Tenga en cuenta que, a diferencia de la mayoría de sistemas de completado, el sistema usa una correspondencia de subsecuencias. Esto significa que puede teclear sólo dos o tres letras de cualquier parte del nombre de archivo para completarlo. Por ejemplo, supongamos que quiere el nombre de archivo ../imagenes/flecha1.png, puede teclear simplemente if1 y pulsar Tab para completar el nombre de archivo. Al buscar coincidencias, el sistema de completado da prioridad a las letras que están al inicio de una palabra, o justo detras de un separador de ruta. Cuando se acostumbre a este sistema, notará que ahorra mucho tiempo y esfuerzo.

Fragmentos

El editor de libros electrónicos de calibre admite fragmentos. Un fragmento es una porción de texto que se reutiliza a menudo o contiene gran cantidad de texto redundante. El editor le permite insertarun fragmento con unas pocas pulsaciones de tecla. Los fragmentos son muy potentes, con muchas funciones como marcadores de posición por los que puede desplazarse, duplicación automática de texto repetido, etc. Para más información, véase Fragmentos.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Modificar libros electrónicos »

Modo de función para buscar y sustituir en el editor

Modo de función para buscar y sustituir en el editor

La herramienta Buscar y sustituir en el editor admite un modo de función. En este modo se pueden combinar expresiones regulares (ver Todo acerca de cómo utilizar expresiones regulares en calibre) con funciones Python de complejidad arbitraria para realizar todo tipo de procesamiento de texto avanzado.

En el modo estándar de expresiones regulares para buscar y sustituir, se especifica una expresión regular para buscar y una plantilla que se usa para sustituir todas las coincidencias. En el modo de función, en lugar de usar una sola plantilla, se especifica una función arbitraria en el lenguaje de programación Python [https://docs.python.org]. Esto le permite hacer muchas cosas que no son posibles sólo con plantillas.

Algunas técnicas para usar el modo de función y su sintaxis se describirán a través de ejemplos, que le mostrarán cómo crear funciones para realizar tareas cada vez más complejas.

Corregir automáticamente las mayúsculas y minúsculas en las cabeceras del documento

Aquí aprovecharemos una de las funciones incorporadas del editor para poner la primera letra de cada palabra dentro de una etiqueta de encabezado en mayúscula:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

Como función, elija la función predefinida Capitalize (ignore tags). Esto cambiará los títulos de la forma <h1>algún TÍTULO</h1> a <h1>Algún título</h1>. Funcionará incluso si hay otras etiquetas HTML en la etiqueta de cabecera.

La primera función personalizada: mejorar guiones

La verdadera potencia del modo de función procede de la posibilidad de crear funciones propias para procesar el texto de manera arbitraria. La herramienta Mejorar puntuación del editor no modifica los guiones aislados, así que puede usar esta función para sustituirlos por rayas.

Para crear una nueva función, simplemente pulse en el botón Crear o modificar y copie el código Python siguiente.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    return match.group().replace('--', '—').replace('-', '—')

Toda función personalizada de Buscar y sustituir debe tener un nombre único y consister en una función Python llamada «replace», que acepte todos los argumentos mostrados arriba. Por ahora no vamos a preocuparnos de los diferentes argumentos de la función replace(). Fíjese sólo en el argumento match. Representa una coincidencia al realizar una búsqueda y sustitución. La documentación completa se encuentra aquí [https://docs.python.org/library/re.html#match-objects]. match.group() devuelve todo el texto de la coincidencia y todo lo que hacemos es sustituir los guiones del texto por rayas, sustituyendo primero los guiones dobles y luego los sencillos.

Use esta función con la expresión regular de búsqueda:

>[^<>]+<

Y sustituirá todos los guiones por rayas, pero sólo en texto real y no dentro de las definiciones de etiqueta HTML.

La potencia del modo de función: usar un diccionario para corregir palabras mal divididas por guiones

A menudo, los libros electrónicos creados a partir de imágenes de libros impresos contienen palabras mal divididas por guiones: palabras que estaban divididas al final de un renglón en la página impresa. Vamos a escribir una función sencilla para encontrar y corregir automáticamente estas palabras.

import regex
from calibre import replace_entities
from calibre import prepare_string_for_xml

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):

    def replace_word(wmatch):
        # Try to remove the hyphen and replace the words if the resulting
        # hyphen free word is recognized by the dictionary
        without_hyphen = wmatch.group(1) + wmatch.group(2)
        if dictionaries.recognized(without_hyphen):
            return without_hyphen
        return wmatch.group()

    # Search for words split by a hyphen
    text = replace_entities(match.group()[1:-1])  # Handle HTML entities like &amp;
    corrected = regex.sub(r'(\w+)\s*-\s*(\w+)', replace_word, text, flags=regex.VERSION1 | regex.UNICODE)
    return '>%s<' % prepare_string_for_xml(corrected)  # Put back required entities

Use esta función con la misma expresión de búsqueda anterior, es decir:

>[^<>]+<

Y corregirá automáticamente todas las palabras con guiones incorrectos en el el texto del libro. El truco principal es utilizar uno de los argumentos adicionales de la función «replace»: dictionaries. Esto se refiere a los diccionarios que el propio editor usa para comprobar la ortografía en el libro. Lo que hace esta función es buscar palabras unidas por un guión, eliminar el guión y comprobar si el diccionario reconoce la palabra compuesta; si lo hace, las palabras originales se sustituyen por la palabra sin guión.

Tenga en cuenta que una limitación de esta técnica es que sólo funciona par libros monolingües, porque de manera predeterminada ``dictionaries.recognized()``usa el idioma principal del libro.

Enumerar secciones automáticamente

Ahora veremos algo un poco diferente. Supongamos que un archivo HTML tiene muchas secciones, cada una con una cabecera en una etiqueta <h2> de esta forma: <h2>Algún texto</h2>. Puede crear una función personalizada que numere automáticamente estas cabeceras con números consecutivos, para que tengan esta forma: <h2>1. Algún texto</h2>.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    section_number = '%d. ' % number
    return match.group(1) + section_number + match.group(2)

# Ensure that when running over multiple files, the files are processed
# in the order in which they appear in the book
replace.file_order = 'spine'

Úselo con la expresión de búsqueda:

(?s)(<h2[^<>]*>)(.+?</h2>)

Coloque el cursor al principio del archivo y pulse Sustituir todo.

Esta función usa otro de los prácticos argumentos adicionales de replace(): el argumento number. Al usar Sustituir todo, number se incrementa automáticamente en cada coincidencia.

Otra característica nueva es el uso de replace.file_order. Establecerlo en 'spine' significa que si la búsqueda se ejecuta sobre múltiples archivos HTML, los archivos se procesarán en el orden en el que aparecen en el libro. Más detalles en Elegir un orden de archivos al ejecutar sobre múltiples archivos HTML.

Crear un índice automáticamente

Por último, vamos a intentar algo un poco más ambicioso. Supongamos que el libro tiene encabezados en etiquetas h1 y h2 del tipo <h1 id="un_id">Algún texto</h1>. Vamos a crear un índice HTML generado automáticamente a partir de estos encabezados. Creamos la siguiente función:

from calibre import replace_entities
from calibre.ebooks.oeb.polish.toc import TOC, toc_to_html
from calibre.gui2.tweak_book import current_container
from calibre.ebooks.oeb.base import xml2str

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    if match is None:
        # All matches found, output the resulting Table of Contents.
        # The argument metadata is the metadata of the book being edited
        if 'toc' in data:
            toc = data['toc']
            root = TOC()
            for (file_name, tag_name, anchor, text) in toc:
                parent = root.children[-1] if tag_name == 'h2' and root.children else root
                parent.add(text, file_name, anchor)
            toc = toc_to_html(root, current_container(), 'toc.html', 'Table of Contents for ' + metadata.title, metadata.language)
            print(xml2str(toc))
        else:
            print('No headings to build ToC from found')
    else:
        # Add an entry corresponding to this match to the Table of Contents
        if 'toc' not in data:
            # The entries are stored in the data object, which will persist
            # for all invocations of this function during a 'Replace All' operation
            data['toc'] = []
        tag_name, anchor, text = match.group(1), replace_entities(match.group(2)), replace_entities(match.group(3))
        data['toc'].append((file_name, tag_name, anchor, text))
        return match.group()  # We don't want to make any actual changes, so return the original matched text

# Ensure that we are called once after the last match is found so we can
# output the ToC
replace.call_after_last_match = True
# Ensure that when running over multiple files, this function is called,
# the files are processed in the order in which they appear in the book
replace.file_order = 'spine'

Y úselo con la expresión de búsqueda:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

Ejecutamos la búsqueda sobre Todos los archivos de texto y al final de la búsqueda se abrirá una ventana con «Salida de depuración de la función», que contendrá el índice HTML, listo par copiarlo en toc.html.

La función de arriba está muy comentada, así que debe ser fácil de seguir. La principal nueva característica es el uso de un argumento adicional de la función replace(), el objeto data. El objeto data es un diccionario Python que sobrevive entre sucesivas llamadas a la función replace() durante una única operación Sustituir todo.

Otra característica nueva es el uso de call_after_last_match. Establecerlo en True en la función replace() significa que el editor ejecutará replace() una vez adicional después de haber encontrado todas las coincidencias. En esa ejecución adicional el objeto de coincidencia será None.

Esto ha sido sólo una demostración de la potencia del modo de función, si realmente necesita generar un índice a partir de encabezados en un libro, es mejor usar la herramienta específica para índices en Herramientas > Índice.

La API para el modo de función

Todas las funciones del modo función deben ser funciones Python con el nombre «replace» y con la siguiente firma:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    return a_string

Cuando se realiza una búsqueda y sustitución, la función replace() se ejecuta para cada coincidencia, debe devolver el texto de sustitución para dicha coincidencia. Si no se ha de efectuar ninguna sustitución, debe devolver match.group(), que es el texto original. Los distintos argumentos de la función replace() se documentan a continuación.

El argumento match

El argumento match representa la coincidencia actual. Es un objeto Match de Python [https://docs.python.org/library/re.html#match-objects]. Su método más útil es group(), que puede emplearse para obtener el texto correspondiente a los grupos de captura individuales en la expresión regular de búsqueda.

El argumento number

El argumento number es el número de la coincidencia actual. Al ejecutar Sustituir todo, cada coincidencia sucesiva da lugar a una ejecución de replace(), con un número que va en aumento. La primera coincidencia tiene el número 1.

El argumento file_name

Éste es el nombre del archivo donde se encontró la coincidencia actual. Al buscar un texto marcado, file_name está vacío. El argumento file_name está en forma canónica: una ruta de acceso relativa a la raíz del libro, usando / como separador.

El argumento metadata

Esto representa los metadatos del libro actual, como título, autores, idioma, etc. Es un objeto de clase calibre.ebooks.metadata.book.base.Metadata. Algunos atributos útiles son title, authors (una lista de autores) y language (el código del idioma).

El argumento dictionaries

Esto representa la colección de diccionarios usados para la comprobación de ortografía del libro actual. Su método más útil es dictionaries.recognized(word), que devuelve True si la palabra word es reconocida por el diccionario para el idioma del libro actual.

El argumento data

Esto es un diccionario de Python sencillo. Al ejecutar Sustituir todo, cada coincidencia sucesiva da lugar a una ejecución de replace() con el mismo data. Por lo tanto puede usarlo para almacenar datos arbitrarios entre ejecuciones de replace() durante una operación de Sustituir todo.

El argumento functions

El argumento functions proporciona acceso a todas las otras funciones definidas por el usuario. Esto es útil para la reutilización de código. Puede definir funciones en un lugar y luego reutilizarlas en todas las otras funciones. Por ejemplo, supongamos que crea una función con nombre My Function así:

def utility():
   # do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

Luego, en otra función, puede acceder a la función utility() de esta manera:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    utility = functions['My Function']['utility']
    ...

También puede usar el objeto functions para guardar datos persistentes que sean accesibles a otras funciones. Por ejemplo, puede tener una función que al ejecutarse con Sustituir todo recopile datos y otra función que los use cuando se ejecuta a continuación. Considere las dos funciones siguientes:

# Function One
persistent_data = {}

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...
    persistent_data['something'] = 'some data'

# Function Two
def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    persistent_data = functions['Function One']['persistent_data']
    ...

Depurar las funciones propias

Puede depurar las funciones que cree utilizando la función estándar de Python print(). La salida de print() se mostrará en una ventana emergente cuando la búsqueda y sustitución se haya completado. Ya hemos visto anteriormente un ejemplo del uso de print() para generar un índice completo.

Elegir un orden de archivos al ejecutar sobre múltiples archivos HTML

Al ejecutar Sustituir todo sobre múltiples archivos HTML, el orden en que se procesan los archivos depende de qué archivos estén abiertos para modificar. Puede hacer que la búsqueda procese los archivos en el orden en el que aparecen estableciendo el attributo file_order de la función, de esta manera:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.file_order = 'spine'

file_order acepta dos valores, spine y spine-reverse, que hacen que el la búsqueda procese archivos múltiples en el orden en que aparecen en el libro, hacia adelante o hacia atrás, respectivamente

Hacer que una función se ejecute una vez más después de la última coincidencia

A veces, como en el ejemplo anterior del índice generado automáticamente, es útil que la función se ejecute una vez adicional después de haber encontrado la última coincidencia. Puede conseguir esto estableciendo el atributo call_after_last_match en la función, de esta manera:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.call_after_last_match = True

Añadir la salida de una función al texto marcado

Al ejecutar una búsqueda y sustitución sobre un texto marcado, a veces es útil añadir algún texto al final del texto marcado. Puede hacer esto estableciéndo el atributo append_final_output_to_marked en la función (tenga en cuenta que también debe establecer call_after_last_match), de esta manera:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...
    return 'some text to append'

replace.call_after_last_match = True
replace.append_final_output_to_marked = True

No mostrar el cuadro de diálogo de resultados al hacer búsquedas en texto marcado

También puede evitar que se muestre el cuadro de diálogo de resultados (que puede ralentizar la aplicación de una búsqueda o sustitución en múltiples bloques de texto) estableciendo el atributo suppress_result_dialog de la función, de esta manera:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
    ...

replace.suppress_result_dialog = True

Más ejemplos

Otros ejemplos útiles, creados por usarios de calibre, pueden encontrarse en el foro del editor libros electrónicos de calibre [https://www.mobileread.com/forums/showthread.php?t=237181] (en inglés).

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Modificar libros electrónicos »

Fragmentos

Fragmentos

El editor de libros electrónicos de calibre admite fragmentos. Un fragmento es una porción de texto que se reutiliza a menudo o contiene gran cantidad de texto redundante. El editor le permite insertar un fragmento con unas pocas pulsaciones de tecla. Por ejemplo, supongamos inserta a menudo etiquetas de enlace cuando modifica archivos HTML, entonces podrá escribir simplemente <a en el editor y pulsar Control+J. El editor lo expandirá a:

<a href="filename"></a>

No sólo eso, la palabra filename estará seleccionada, con el cursor sobre ella, para que pueda teclear el nombre de archivo real usando la función Completado automático del editor. Y una vez que haya terminado de escribir el nombre de archivo, pulse Control+J de nuevo y el cursor saltará a la posición entre las etiquetas <a> para que pueda teclear directamente el texto par el enlace.

El sistema de fragmentos del editor es muy sofisticado, hay algunos fragmentos predefinidos y puede crear otros adecuados para su estilo de modificación.

La siguiente discusión sobre los fragmentos predefinidos debería ayudar a ilustrar la potencia del sistema de fragmentos.

Nota

También puede usar fragmentos en los campos de entrada de texto del panel :guilabel: Buscar y sustituir, pero los marcadores de posición (para pasar por ellos usando Control+J) no funcionarán.

Los fragmentos predefinidos

Los fragmentos predefinidos se definen a continuación. Tenga en cuenta que puede reemplazarlos creando fragmentos propios con el mismo texto de activación.

Insertar texto de relleno [Lorem]

El primer fragmento predefinido y el más simple, se usa para insertar texto de relleno en un documento. El texto de relleno se toma de De finibus bonorum et malorum [https://es.wikipedia.org/wiki/De_finibus], una obra filosófica de Cicerón (traducida al inglés). Para usarlo, simplemente teclee Lorem en un archivo HTML y pulse Control+J. Se sustituirá por un par de párrafos de relleno.

La definición de este fragmento es muy simple, el texto de activación se define como Lorem y la plantilla no es más que el texto literal que se inserta. Es muy sencillo personalizarlo par usar el texto de relleno que prefiera.

Insertar una etiqueta HTML con cierre incorporado [<>]

Veamos un ejemplo sencillo del potente concepto de marcadores de lugar. Digamos que quiere insertar la etiqueta con cierre incorporado <hr/>. Únicamente teclee <> y pulse Control+J, el editor expandirá el fragmento a:

<|/>

Aquí, el símbolo | representa la posición actual del cursor. Ahora puede teclear hr y pulsar Control+J para mover el cursor al final de la etiqueta. Este fragmento está definido como:

Trigger: <>
Template: <$1/>$2

Los marcadores de posición son simplemente el símbolo de dólar ($) seguido de un número. Cuando el fragmento se expande al pulsar Control+J el cursor se coloca en el primer marcador de posición (el que tenga el número más bajo). Cuando pulsa Control+J otra vez el cursor se desplaza al siguiente marcador de posición (el que tenga el siguiente número más bajo).

Insertar un enlace HTML [<a]

Todas las etiquetas de enlace HTML tienen una estructura común. Tienen un atributo href y algún texto entre las etiquetas de apertura y cierre. Vamos a ver otras características de los marcadores de posición con un fragmento que hará más eficiente teclear estas etiquetas. Para usar este fragmento, simplemente teclee <a y pulse Control+J. El editor lo expandirá a:

<a href="filename|"></a>

No sólo eso, la palabra filename estará seleccionada, con el cursor sobre ella, para que pueda teclear el nombre de archivo real usando la función Completado automático del editor. Y una vez que haya terminado de escribir el nombre de archivo, pulse Control+J de nuevo y el cursor saltará a la posición entre las etiquetas <a> para que pueda teclear directamente el texto par el enlace. Cuando termine de escribir el texto, pulse Control+J otra vez para saltar a la derecha de la etiqueta de cierre. Este fragmento está definido como:

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

Hay un par de nuevas características. Primero, el marcador $1 se ha vuelto más complejo. Ahora incluye un texto predeterminado (la palabra filename). Si un marcador de posición contiene un texto predeterminado, el marcador se sustituye por el texto al expandir el fragmento. Además, al saltar a un marcador con texto predeterminado usando Control+J, el texto se selecciona. De esta manera, puede usar el texto predeterminado como un recordatorio para rellenar las partes importantes de la plantilla. Puede especificar un texto predeterminado para un marcador de posición usando la sintaxis: ${<número>:texto predeterminado}.

La otra nueva característica es que el segundo marcador de posición tiene un asterisco al final (${2*}). Esto significa que cualquier texto que estuviera seleccionado antes de expandir la plantilla sustituirá al marcador. Para verlo en acción, seleccione algún texto en el editor, pulse Control+J, teclee <a y pulse Control+J otra vez, la plantilla se expandirá a:

<a href="filename">whatever text you selected</a>

Insertar una etiqueta de imagen HTML [<i]

Esto es muy parecido a insertar un enlace HTML, como hemos visto anteriormente. Le permite introducir rápidamente <img src="filename" alt="description" /> y saltar entre los atributos src y alt:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

Insertar una etiqueta HTML arbitraria [<<]

Esto le permito insertar una etiqueta completa HTML arbitraria (o incluir un texto previamente seleccionado en la etiqueta). Para usarlo, teclee << y pulse Control+J. El editor lo expandirá a:

<|></>

Teclee el nombre de la etiqueta, por ejemplo span, y pulse Control+J, el resultado será:

<span>|</span>

Verá que la etiqueta de cierre se ha rellenado automáticamente con span. Esto se obtiene gracias a otra característica de los marcadores de posición, la duplicación. Duplicación significa simplemente que si especifica el mismo marcador más de una vez en una plantilla, la segunda posición y las posteriores se rellenarán automáticamente, al pulsar Control+J, con lo que haya tecleado en la primera posición. La definición para este fragmento es:

Trigger: <<
Template: <$1>${2*}</$1>$3

Como puede ver, el primer marcador de posición ($1) se ha especificado dos veces, la segunda en la etiqueta de cierre, lo que sencillamente copiará lo que sea que haya introducido en la etiqueta de apertura.

Insertar una etiqueta HTML arbitraria con un atributo de clase [<c]

Esto es muy parecido al ejemplo anterior de inserción de una etiqueta arbitraria, excepto que supone que querrá especificar una clase para la etiqueta:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

Esto le permitirá teclear primero el nombre de la etiqueta, pulsar Control+J, teclear el nombre de clase, pulsar Control+J, teclear el contenido de la etiqueta y pulsar Control+J una última vez para saltar fuera de la etiqueta. La etiqueta de cierre se rellenará automáticamente.

Crear fragmentos propios

Lo mejor de los fragmentos es que puede crear los suyos propios que mejor se adapten a su estilo. Para crear fragmentos propios vaya a Editar > Preferencias > Configuración del editor > Administrar fragmentos en el editor. Esto mostrará un cuadro de diálog fácil de usar que le ayudará a crear fragmentos. Pulse en el botón Añadir fragmento y verá un cuadro de diálogo parecido a:

Primero asigne un nombre al fragmento, algo descriptivo, para ayudarle a identificar el fragmento en el futuro. Luego especifique el activador. Un activador no es más que el texto que debe teclear en el editor antes de pulsar Control+J para expandir el fragmento.

Después especifique la plantilla del fragmento. Es recomendable que empiece con alguno de los ejemplos anteriores y lo modifique según sus necesidades. Finalmente, especifique en qué tipos de archivo quiere que esté disponible el fragmento. De esta forma puede tener varios fragmentos con el mismo texto activador que funcionan de manera distinta en distintos tipos de archivo.

El siguiente paso es probar el fragmento recién creado. Use el cuadro Prueba en la parte inferior. Teclee el texto activador y pulse Control+J para expandir el fragmento y para saltar entre los marcadores de posición.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

El servidor de contenidos de calibre

El servidor de contenidos de calibre

El Servidor de contenidos de calibre le permite acceder a las bibliotecas de calibre y leer libros directamente en un navegador, en un teléfono móvil o en un dispositivo portátil. Por lo tanto, no necesita instalar ninguna aplicación específica para leer o administrar libros. No tiene más que usar el navegador. El servidor descarga y almacena el libro que esté leyendo en un caché local, así que puede leer incluso si no tiene conexión a Internet.

Índice

Acceder al servidor de contenidos desde otros dispositivos

Acceder al servidor desde dispositivos en la red local

Acceder al servidor desde cualquier parte en Internet

La interfaz del servidor

La lista de libros

El lector de libros

Soporte de navegadores

Activar modo sin conexión

Administrar cuentas de usuario en la línea de órdenes

Integrar el servidor de contenidos de calibre en otros servidores

Usar un servidor virtual completo

Usar un prefijo de URL

Crear un servicio para el servidor de calibre en un sistema Linux moderno

Para iniciar el servidor, pulse en el botón Conectar y compartir y elija Iniciar servidor de contenido. Puede que reciba un mensaje del programa antivirus o cortafuegos del equipo preguntando si permite el acceso a calibre.exe. Pulse el botón Permitir o Aceptar. Después abra el navegador (preferentemente Chrome o Firefox) en el equipo y teclée la siguiente dirección:

http://127.0.0.1:8080

Esto abrirá una página en el navegador mostrando las bibliotecas de calibre, pulse en cualquiera de ellas y explore sus libros. Pulse en un libro y le mostrará todos los metadatos del libro junto con botones para Leer y Descargar el libro. Pulse el botón Leer para empezar a leer el libro.

Nota

La dirección usada anteriormente, http://127.0.0.1:8080, sólo funcionará en el equipo que está ejecutando calibre. Para acceder al servidor desde otro equipo, teléfono, etc. tendrá que realizar alguna acción adicional, como se describe en la siguiente sección.

Acceder al servidor de contenidos desde otros dispositivos

Hay dos tipos de acceso desde dispositivo remoto que normalmente necesitará. El primer tipo, más simple, es desde la red local. Si está ejecutando calibre en la red local y también ha conectado otros dispositivos a la misma red, debería poder acceder fácilmente al servidor desde estos dispositivos.

Acceder al servidor desde dispositivos en la red local

Una vez iniciado el servidor en calibre como se describe más arriba, pulse el botón Conectar y compartir de nuevo. En lugar de la acción Iniciar servidor de contenido debería ver una acción Detener servidor de contenido. A la derecha de esta acción habrá una dirección IP y número de puerto. Se trata de un conjunto de números separados por puntos. Por ejemplo:

Stop Content server [192.168.1.5, port 8080]

Estos números le dicen qué direcciones usar para conectar al servidor en los dispositivos. Siguiendo los ejemplos anteriores, la dirección resulta:

http://192.168.1.5:8080

La primera parte de la dirección es siempre http://, la siguiente parte es la dirección iP, que son los números antes de la coma, y finalmente tenemos el número de puerto, que debe añadirse a la dirección IP con dos puntos (:). Si tiene suerte, eso debería ser todo lo que necesite y ya estará viendo las bibliotecas de calibre en el dispositivo. Si no, siga leyendo.

Resolución de problemas de conexión en la red local

Si no es capaz de acceder al servidor desde el dispositivo, pruebe los siguientes pasos:

Compruebe que el servidor está ejecutándose abriendo la dirección http://127.0.0.1:8080 en un navegador en el mismo equipo que el servidor.

Compruebe que el cortafuegos o antivirus permite conexiones al equipo en el puerto 8080 y al programa calibre. La manera más sencilla de descartar el cortafuegos o antivirus como fuente de problemas es desactivarlos temporalmente y probar la conexión. Antes de desactivar el cortafuegos, para mantener el equipo a salvo, debería desconectarlo de Internet.

Compruebe que el dispositivo y el equipo están en la misma red. Esto significa que ambos deberían estar conectado al mismo enrutador inalámbrico. En particular, ninguno debería estar usando una conexión de telefonía o conexión wifi directa con el proveedor.

Si tiene una configuración de red no convencional, puede ser que la dirección IP mostrada en el menú Conectar y compartir sea incorrecta. En tal caso deberá averiguar cuál es la dirección IP correcta para usar por sí mismo. Por desgracia, dada la infinita variedad de configuraciones de red posibles, no es posible ofrecerle una guía para hacerlo.

Si ha configurado un nombre de usuario y contraseña, pruebe primero sin ellos para comprobar si están causando problemas. Algunos dispositivos de lectura tienen navegadores que no admiten autenticación. A veces puede solucionarlo incluyendo el nombre de usuario y la contraseña en el URL, por ejemplo: http://usuario:contraseña@192.168.1.2:8080.

Si se queda atascado, puede pedir ayuda en los foros de usuarios de calibre [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Acceder al servidor desde cualquier parte en Internet

Advertencia

Antes de hacer esto, debe activar la protección de nombre de usuario y contraseña en el servidor, en caso contrario cualquier en cualquier parte del mundo podrá acceder a sus libros. Vaya a Preferencias > Compartir > Compartir por la red y active la opción Requerir nombre de usuario y contraseña para acceder al servidor de contenidos.

Aunque los detalles concretos para la configuración del acceso por Internet dependen de la configuración de red y del tipo de equipo que esté usando, el esquema básico es el siguiente.

Averigüe la dirección IP externa del equipo en el que va a ejecutar el servidor. Puede hacer esto visitando la página What is my IP address [https://www.whatismyip.com/] en un navegador en el mismo equipo.

Si el equipo está tras un enrutador, active el redireccionado de puertos en el enrutador para dirigir el puerto 8080 (o el puerto que elija para ejecutar el servidor de contenidos) al equipo.

Asegúrese de que el servidor de calibre no está bloqueado por ningún cortafuegos o antivirus en el equipo.

Ahora deberá poder acceder al servidor desde cualquier dispositivo conectado a Internet usando la dirección IP que encontró en el primer paso. Por ejemplo, si la dirección IP es 123.123.123.123 y el puerto que está usando para el servidor de calibre es 8080, la dirección que debe usar en el dispositivo será: http://123.123.123.123:8080.

De manera opcional, use un servicio como no-ip [https://www.noip.com/free] para configurar una dirección fácil de recordar en lugar de la dirección IP que encontró en el primer paso.

Nota

Para máxima seguridad, debería también activar HTPPS en el servidor de contenidos. Puede hacer lo directamente en el servidor suministrando la ruta de acceso al certificado HTTPS para usar en las opciones de configuración avanzada del servidor, o puede configurar un proxy inverso como se describe a continuación, para usar una configuración HTTPS ya existente.

La interfaz del servidor

La interfaz del servidor es una versión simplificada de la interfaz principal de calibre, optimizada para su uso con pantallas táctiles. La pantalla principal muestra los libros que está leyendo actualmente y le permite elegir una biblioteca de calibre para explorar. El servidor de calibre le da acceso a todas las bibliotecas, no sólo a una como ocurría con anterioridad.

La lista de libros

La lista de libros del servidor es un cuadro de portadas. Pulse en una portada para ver los metadatos del libro o leer el libro. Si prefiere una lista más detallada puede cambiar la vista predeterminada pulsando en los tres puntos verticales en la esquina superior derecha.

Ordenar y buscar la lista de libros deberían ser opciones conocidas para los usuarios de calibre. Se puede acceder a ellas pulsando en sus iconos en la zona superior derecha. Funcionan exactamente igual que en el programa principal de calibre. La página de búsqueda le permite incluso construir consultas de búsqueda pulsando sobre autores, etiquetas, etc., como cuando se usa el explorador de etiquetas en el programa principal.

Las Bibliotecas virtuales, opción muy apreciada en el programa principal, existen también en la interfaz del servidor. Pulse en los tres puntos verticales en la esquina superior derecha para elegir una biblioteca virtual.

El lector de libros

Puede leer cualquier libro en la biblioteca de calibre simplemente pulsando sobre él y luego en el botón Leer. El lector de libros es muy fácil de manejar. Puede pulsar o deslizar para pasar páginas. Si desliza arriba o abajo saltará capítulos. Al pulsar en el cuarto superior de la pantalla se muestran los controles detallados y las preferencias del visor.

Puede dejar el servidor de contenidos ejecutándose, incluso puede abrir el mismo libro en varios dispositivos y se mantendrá la última posición de lectura. Si no es así, puede forzar la sincronización pulsando en el cuarto superior y eligiendo Sincronizar.

Soporte de navegadores

El nuevo servidor de calibre hace uso de características avanzadas HTML 5 y CSS 3. Por lo tanto, requiere el uso de un navegador actualizado. Se ha probado en Chrome sobre Android y Safari sobre iOS, así como en Chrome y Firefox en el escritorio.

El servidor procura usar características que han sido aceptadas como estándar o están en el camino. Por lo tanto, si actualmente no funciona en su navegador preferido, probablemente funcione una vez que el navegador se haya actualizado.

Si está usando un navegador antiguo o limitado o si no quiere ejecutar JavaScript, pude usar la vista móvil, simplimente añadiendo /mobile a la dirección del servidor.

Nota

En iOS, Apple permite un único motor de navegador, así que Firefox, Chrome y Safari son en realidad el mismo navigador en su interior. La nueva interfaz del navegador requiere iOS 10.3.2 o posterior. En Android, el servidor se ha probado con Chrome, versión 58 y posterior.

Activar modo sin conexión

Los creadores de navegadores han estado tratando de hacer que la gente use SSL a base de desactivar funciones avanzadas en los navegadores para conexiones HTTP. Una de estas bajas es ApplicationCache, que es lo que usaba calibre para el modo sin conexión. Como resultado, por desgracia en la actualidad, el modo sin conexión funciona sólo mientras esté abierta la pestaña del navegador. Además, en Firefox para Android, tendra que visitar about:config, crear una preferencia con el nombre browser.tabs.useCache y activarla.

Administrar cuentas de usuario en la línea de órdenes

El programa calibre tiene una sección en Preferencias que le permite administrar las cuentas de usuario del servidor. Sin embargo, si quiere ejecutar el servidor independiente y no puede ejectuar el programa principal de calibre en el mismo equipo o con el mismo usuario, también puede administrar las cuentas de usuario en la línea de órdenes.

Puede administrar las cuentas de usuario con la opción --manage-users del programa independiente calibre-server. Supongamos que quiere almacenar la base de datos de usuarios en la carpeta /srv/calibre, la puede crear ejecutando:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

Siga las indicaciones para crear cuentas de usuario, asignar permisos, etc. Cuando termine, puede ejecutar el servidor como:

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

Usará las cuentas de usuario creadas en el paso anterior.

Integrar el servidor de contenidos de calibre en otros servidores

Aquí le mostraremos cómo integrar el servidor de contenidos de calibre en otro servidor. La razón más común para esto es hacer uso de SSL o para incorporar la biblioteca de calibre como parte de otra página. La técnica básica es ejecutar el servidor de calibre y configurar un proxy inverso a éste desde el servidor principal.

Un proxy inverso es cuando el servidor principal acepta peticiones entrantes y las traslada al servidor de calibre. A continuación, lee la respuesta desde el servidor de calibre y la envía al cliente. Esto significa que sólo tiene que ejecutar el servidor de calibre de forma normal sin tratar de integrarlo estrechamente con el servidor principal.

Usar un servidor virtual completo

La configuración más sencilla es dedicar un servidor virtual completo al servidor de calibre. En este caso, ejecute el servidor de calibre como:

calibre-server

Ahora configure el servidor virtual en el servidor principal, por ejemplo, para nginx:

http {
    client_max_body_size 64M;  # needed to upload large books
}

server {
    listen [::]:80;
    server_name myserver.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

O, para Apache:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

<VirtualHost *:80>
    ServerName myserver.example.com
    AllowEncodedSlashes On
    ProxyPreserveHost On
    ProxyPass "/"  "http://localhost:8080/"
</VirtualHost>

Usar un prefijo de URL

Si no quiere dedicar un servidor virtual completo a calibre, puede hacer que use un prefijo de URL. Inicie el servidor de calibre como:

calibre-server --url-prefix /calibre --port 8080

El parámetro clave aquí es --url-prefix /calibre. Esto hace que el servidor de contenidos sirva todos los URL con el prefijo calibre. Para verlo en acción, visite http://localhost:8080/calibre en su navegador. Debería ver la página web habitual del servidor de contenido, pero ahora se ejecutará desde /calibre.

Con nginx, la configuración necesaria es:

http {
    client_max_body_size 64M;  # needed to upload large books
}

proxy_set_header X-Forwarded-For $remote_addr;
location /calibre/ {
    proxy_buffering off;
    proxy_pass http://127.0.0.1:8080$request_uri;
}
location /calibre {
    # we need a trailing slash for the Application Cache to work
    rewrite /calibre /calibre/ permanent;
}

Para Apache, primero active los módulos de proxy añadiendo lo siguiente a httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

La técnica exacta para habilitar los módulos proxy variará según la instalación de Apache. Una vez que tenga los módulos proxy habilitados, agregue las siguientes reglas a httpd.conf (o, si está usando servidores virtuales, al archivo conf del servidor virtual en cuestión):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

Eso es todo, ahora podrá acceder al servidor de contenidos de calibre bajo el URL /calibre en su servidor principal. Las reglas anteriores pasan todas las peticiones bajo /calibre al servidor de calibre que se ejecuta en el puerto 8080 y gracias a la opción --url-prefix mencionada anteriormente, el servidor de calibre las gestiona de forma transparente.

Nota

Al usar un proxy inverso, debe decirle al servidor de contenidos de calibre que atienda sólo a peticiones del servidor local, usando --listen-on 127.0.0.1. De esta forma, el servidor sólo recibirá conexiones del mismo equipo, es decir, del proxy inverso.

Nota

Si ha configurado autenticación SSL en el servidor principal, debe decirle a calibre que use autenticación básica en lugar de «digest», pues es más rápida. Para ello, pase la opción --auth-mode=basic a calibre-server.

Crear un servicio para el servidor de calibre en un sistema Linux moderno

Crear un servicio para ejecutar calibre en el arranque de un sistema basado en Linux moderno (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) es sencillo. No tiene más que crear el archivo /etc/systemd/system/calibre-server.service con el siguiente contenido:

[Unit]
Description=calibre Content server
After=network.target

[Service]
Type=simple
User=mylinuxuser
Group=mylinuxgroup
ExecStart=/opt/calibre/calibre-server "/path/to/calibre library folder"

[Install]
WantedBy=multi-user.target

Cambio mylinuxuser y mylinuxgroup al usuario y grupo bajo los que quiere que se ejecute el servidor. Debería ser el mismo usuario y grupo al que pertenecen los archivos en la carpeta de la biblioteca de calibre. Tenga en cuenta que no suele ser una buena idea ejecutar el servidor como superusuario. Cambie también la ruta de acceso a la carpeta de la biblioteca de calibre según su caso. Puede añadir varias bibliotecas si lo desea. Vea la ayuda para la orden calibre-server.

Ahora ejecute:

sudo systemctl start calibre-server

para iniciar el servidor. Compruebe su estado con:

sudo systemctl status calibre-server

Para hacer que se inicie en el arranque, ejecute:

sudo systemctl enable calibre-server

Nota

El servidor de calibre no necesita un servidor X en ejecución, pero necesita tener instaladas las bibliotecas X, pues algunos de los componentes que usa enlazan con ellas.

Nota

El servidor de calibre también permite activación de sockets systemd, así que también puede usar esta opción, si es necesario.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Comparar libros electrónicos

Comparar libros electrónicos

calibre incluye una herramienta integrada de comparación de libros electrónicos que se puede utilizar para ver lo que ha cambiado en el interior de un libro electrónico después de editarlo o convertirlo. Puede comparar libros en los formatos EPUB y AZW3.

Para usarla, sea abrir el libro en la herramienta para Modificar libros electrónicos y después pulse en Archivo > Comparar con otro libro o use el panel Detalles del libro. Si hace una conversion de EPUB a EPUB, el archivo EPUB original se guardará como ORIGINAL_EPUB. Simplemente pulse con el botón derecho en la entrada ORIGINAL_EPUB en el panel de los detalles del libro y seleccione Comparar con formato EPUB.

La herramienta de comparación que se abre será parecida a la siguiente imagen. Muestra las diferencias en el texto, estilos e imágenes entre los libros elegidos.

Comprender la vista de comparación

Como se puede ver en la imagen anterior, la vista de comparación muestra las diferencias entre los dos libros uno junto a otro. Sólo se muestran las diferencias con unas pocas líneas de contexto a su alrededor. Esto hace que sea fácil ver de un vistazo sólo lo que ha cambiado dentro de un documento de gran tamaño como un libro.

El texto añadido se muestra con un fondo verde, el texto eliminado con un fondo rojo y el texto modificado con un fondo azul.

Los números de línea de todo el texto modificado se muestran en los lados, por lo que es fácil desplazarse a un cambio determinado en el editor. Al abrir la herramienta de comparación desde el editor, también puede pulsar dos veces en una línea en el panel de la derecha para ir a esa línea en el editor de forma automática.

Una técnica útil cuando se comparan libros es decirle a la herramienta de comparación que embellezca los archivos de texto y estilo antes de calcular las diferencias. Esto a menudo puede resultar en diferencias más limpias y más faciles de seguir. Para ello, pulse en el botón :guilabel: Opciones en la parte inferior derecha y elija Embellecer los archivos antes de compararlos. Tenga en cuenta que el embellecimiento a veces puede tener efectos no deseados, ya que puede causar que un código no válido pase a ser válido. También puede cambiar el número de líneas de contexto que se muestran en torno a las diferencias mediante el botón :guilabel: Opciones.

Puede buscar cualquier texto en las diferencias a través de la barra de búsqueda en la parte inferior. Tendrá que especificar en qué panel buscar, el Izquierdo o el Derecho.

Iniciar la herramienta de comparación

La herramienta de comparación es más útil cuando se tienen dos versiones de un mismo libro y se desea ver lo que es diferente entre ellos. Hay varias formas de iniciar la herramienta.

Comparar dos archivos de libro electrónico

Abra el primer archivo en la herramienta Modificar libros electrónicos. Ahora pulse en Archivo > Comparar con otro libro y elija el segundo archivo (debe estar en el mismo formato que el primero). La vista de comparación se abrirá con el archivo que se está modificando a la derecha y el otro a la izquierda.

Comparar el ORIGINAL_FMT con FMT

Cuando realiza una conversión en calibre desde un FMT a sí mismo, el archivo original es guardado como ORIGINAL_FMT. Puede ver qué ha cambiado en la conversión, pulsando con el botón derecho en la entrada ORIGINAL_FMT en el panel Detalles del libro de la ventana principal de calibre y seleccionando Comparar con FMT. La vista de comparación se abrirá con el ORIGINAL_FMT a la izquierda y el FMT a la derecha.

Comparar un hito con el estado actual del libro durante su modificación

La herramienta Modificar libros electrónicos posee una función muy útil, llamada Hitos. Esto le permite guardar el estado actual de un libro como un hito, al cual puede retornar si no le agrada los cambios que ha hecho desde que se creó el hito. Estos hitos también se crean automáticamente cuando realizan algunas acciones automatizadas. Puede ver la lista de hitos en Ver > Hitos y después puede usar el botón Comparar para comparar el libro en el hito seleccionado con el estado actual. La herramienta de comparación mostrará el hito a la izquierda y el estado actual a la derecha.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Modificar los metadatos de los libros

Modificar los metadatos de los libros

Índice

Editar metadatos de libros, de uno en uno

Descargar metadatos

Gestionar formatos de libros

Todo sobre las portadas

Modificar los metadatos de varios libros a la vez

Buscar y sustituir

Descargar metadatos en masa

Añadir archivos de datos adicionales a un libro

Los libros electrónicos vienen en todas formas y tamaños, y en la mayoría de las ocasiones sus metadatos (tales como título, autor, serie, editorial) son incompletos o incorrectos. La forma más sencilla de cambiar los metadatos en calibre es simplemente pulsar dos veces sobre una entrada y escribir el dato correcto. Para un modificación más sofisticada, utilice las herramientas de edición de metadatos discutidas a continuación.

Editar metadatos de libros, de uno en uno

Pulse en el libro que desea editar y luego en el botón Modificar metadatos o pulse la tecla E. Se abrirá un cuadro de diálogo que le permite modificar toda clase de metadatos. Tiene diversas funciones para hacer la modificación más rápida y eficiente. Una lista de los consejos más prácticos:

Puede pulsar en el botón entre título y autores para intercambiarlos automáticamente.

Puede pulsar en botón junto al orden de autor para que calibre rellene el campo automáticamente usando el valor de orden almacenado para cada autor. Utilice el cuadro de diálogo Administrar autores para ver y cambiar los valores de orden de autores. Este cuadro de diálogo se puede abrir manteniendo pulsado el botón junto al orden de autor.

Puede pulsor en el botón junto a las etiquetas para usar el Editor de etiquetas para gestionar las etiquetas asociadas con el libro.

El cuadro «Ids» puede usarse para introducir un ISBN (y muchos otros tipos de identificadores), tendrá un fondo rojo si introduce un ISBN no válido. Será verde para los ISBN válidos.

El cuadro de orden de autor será de color rojo si el valor del orden de autor difiere de lo que calibre piensa que debería ser.

Descargar metadatos

La mejor característica del cuadro de diálogo de modificación de metadatos es la posibilidad de rellenar automáticamente muchos de os campos de metadatos al obtenerlos de diversos sitios de Internet. Actualmente, calibre utiliza Google Books y Amazon. La descarga de metadatos puede rellenar el título, autor, serie, etiquetas, calificación, descripción e ISBN.

Para utilizar la descarga, rellene los campos de título y autor y pulse en el botón Descargar metadatos. calibre le presentará una lista de los libros que mejor coinciden con el título y el autor. Si rellena el campo ISBN primero, se le dará prioridad con respecto respecto al título y autor. Si no se encuentran coincidencias, trate de hacer su búsqueda un poco menos específica incluyendo sólo algunas palabras clave en el título y sólo el apellido del autor.

Gestionar formatos de libros

En calibre, una sola entrada de libro puede tener varios formatos diferentes asociados. Por ejemplo, puede haber obtenido las obras completas de Cervantes en formato EPUB y haberlas convertido más tarde a MOBI para leerlas en el Kindle. calibre gestiona automáticamente múltiples formatos. En la sección Formatos disponibles del cuadro de diálogo Modificar metadatos, puede administrar estos formatos. Puede agregar un nuevo formato, borrar uno existente y también solicitar a calibre que obtenga los metadatos y la portada del libro a partir de los metadatos contenidos en alguno de los formatos.

Todo sobre las portadas

Puede hacer que calibre descargue las portadas de los libros, siempre y cuando el libro posea un ISBN conocido. Alternativamente, puede especificar un archivo en el equipo para utilizarlo como portada. calibre puede incluso generar una portada predeterminada con metadatos básicos. Puede arrastrar y soltar imágenes sobre la portada para cambiarla o hacer click con el botón derecho para copiar y pegar imágenes.

Además, hay un botón para recortar automáticamente los bordes de la portada, en el caso que la imagen de la portada tenga un borde antiestético.

Modificar los metadatos de varios libros a la vez

Primero seleccione los libros que desea editar manteniendo presionada la tecla Ctrl o Mayús y pulsando sobre ellos. Si selecciona más de un libro, pulse en el botón Modificar metadatos lo que provocará que se abra el cuadro de diálogo de Modificar metadatos en masa. Usando este cuadro, puede establecer rápidamente el autor, editorial, calificación, etiquetas, series, etc de un conjunto de libros con el mismo dato. Esto es particularmente útil si acaba de importar es una serie de libros que tienen varios metadatos en común. Esta función es muy potente, por ejemplo, tiene una pestaña de Buscar y sustituir que se puede utilizar para llevar a cabo operaciones masivas sobre los metadatos e incluso copiar los metadatos de una columna a otra.

El cuadro de diálogo de modificar metadatos normal también posee los botones Siguiente y Anterior que puede utilizar para modificar los metadatos de varios libros uno tras otro.

Buscar y sustituir

El cuadro de diálogo Modificar metadatos de varios archivos le permite realizar operaciones de búsqueda y sustitución de complejidad arbitrara en los libros seleccionados. De manera predeterminada usa una búsqueda y sustitución simple de texto, pero también admite expresiones regulares. Para más información sobre las expresiones regulares, consulte Todo acerca de cómo utilizar expresiones regulares en calibre.

Como se señaló anteriormente, hay dos modos de buscar y sustituir: coincidencia de caracteres y expresión regular. Coincidencia de caracteres buscará en el Campo de búsqueda que elija los caracteres que escriba en el cuadro Buscar y las sustituirá por los que escriba en el cuadro Sustituir por. Todas las coincidencias de los caracteres buscados en el campo se sustituirán. Por ejemplo, supongamos que el campo que se está buscado contiene un gato malo. Si busca a para sustituirla por HOLA, el resultado será un gHOLAto mHOLAlo.

Si el campo en el que está buscando es un campo múltiple como las etiquetas, entonces cada etiqueta se considera separadamente. Por ejemplo, si las etiquetas contienen Horror, Miedo, la expresión de búsqueda r, no encontrará nada, porque la expresión se aplicará primero a Horror y después a Miedo.

Si desea que la búsqueda ignore las diferencias entre mayúsculas y minúsculas, desactive la casilla Distinguir mayúsculas.

Puede hacer que calibre cambie las mayúsculas y minúsculas de los resultados de las búsquedas (información después de sustituir) eligiendo una de las funciones del cuadro Aplicar función después de sustituir. Las operaciones disponibles son:

Minúsculas – cambia todos los caracteres del campo a minúsculas

Mayúsculas – cambia todos los caracteres del campo a mayúsculas

Mayúsculas iniciales – cambia a mayúscula la primera letra de cada palabra en el resultado.

En el cuadro Su prueba puede introducir texto para comprobar que el efecto de buscar y sustituir es el deseado. En la mayoría de los casos, los cuadros de prueba de libro serán suficiente, pero es posible que haya un caso que quiera comprobar y que no se muestre en estos cuadres. Intruduzca dicho caso en Su prueba.

El modo de expresión regular tiene algunas diferencias con respecto al modo de caracteres, más allá, evidentemente, del hecho de usar expresiones regulares. La primera es que las funciones se aplican a las partes del texto que coincidan con el texto de búsqueda, no a todo el campo. La segunda es que las funciones se aplican al texto de sustitución, no a todo el campo.

La tercera y más importante es que el texto de sustitución puede hacer referencia a partes del texto de búsqueda mediante el uso de retroreferencias. Una retroreferencia es \\n donde n es un entero que se refiere al grupo entre paréntesis número n en la expresión de búsqueda. Por ejemplo, dado el mismo ejemplo anterior, un gato malo, una expresión de búsqueda un (….) (….), y una expresión de reemplazo un \2 \1, el resultado será un malo gato. Consulte el Todo acerca de cómo utilizar expresiones regulares en calibre para obtener más información acerca de las retroreferencias.

Una pauta útil: supongamos que desea cambiar las mayúsculas y minúsculas de un campo entero. La forma más sencilla de hacerlo es utilizar el modo de caracteres, pero supongamos que desea utilizar el modo de expresión regular. La expresión de búsqueda debe ser (^.*$) la expresión de sustitución debe ser \1, y tendrá que seleccionar la función de cambio de mayúsculas y minúsculas que desee.

Finalmente, en el modo de expresión regular puede copiar los valores de un campo a otro. Simplemente ponga diferentes campos en origen y destino. La copia puede sustituir el campo de destino, anteponerse al campo (añadirse al principio), o añadirse al campo (añadirse al final). La casilla de verificación «usar coma» hace que calibre añada (o no) una coma entre el texto y el campo de destino en los modos de anteponer y añadir. Si el destino es múltiple (por ejemplo, etiquetas), entonces no se puede desactivar esta casilla.

La búsqueda y sustitución se realiza después de aplicar todos los cambios en los metadatos de las otras pestañas. Esto puede llevar a cierta confusión, ya que los cuadros de prueba mostrarán la información anterior a los otros cambios, pero la operación se aplicará después de ellos. Si tiene alguna duda sobre lo que pueda suceder, no mezcle buscar y sustituir con otros cambios.

Descargar metadatos en masa

Si quiere descargar los metadatos de varios libros a la vez, pulse en el botón Modificar metadatos y seleccione Descargar metadatos. Puede elegir descargar sólo metadatos, sólo portadas o ambos.

Añadir archivos de datos adicionales a un libro

calibre puede almacenar cualquier número de archivos de datos adicionales asociados a un libro. Pueden ser portadas alternativas, material complementario, etc. No se pueden ver directamente ni utilizar como fuentes de conversión. Tampoco están indexados por el motor de búsqueda de texto completo de calibre. Para verlos, añadirlos o eliminarlos, seleccione el libro y pulse con el botón derecho en el botón Modificar metadatos y seleccione Administrar archivos de datos. Esto abrirá una ventana emergente donde puede realizar operaciones con estos archivos. Alternativamente, puede pulsar con el botón derecho en el botón Añadir libros y elegir Añadir archivos de datos a los registros de libros seleccionados para añadir archivos de datos más rápidamente.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Preguntas frecuentes

Preguntas frecuentes

Índice

Conversión de formatos de libro electrónico

Integración con dispositivos

Gestión de la biblioteca

Miscelánea

Conversión de formatos de libro electrónico

Índice

¿Qué formatos puede convertir calibre?

¿Cuáles son los mejores formatos de origen para convertir?.

He convertido un archivo PDF, pero el resultado tiene varios problemas.

¿Cómo convierto un archivo que contiene caracteres «exóticos» (acentuados, no latinos, comillas tipográficas, etc.)?

¿Qué ocurre con los índices de los archivos MOBI?

¿Cómo convierto una colección de archivos HTML en un orden específico?

El EPUB que he creado con calibre no es válido

¿Cómo puedo utilizar algunas de las funciones avanzadas de las herramientas de conversión?

¿Qué formatos puede convertir calibre?

calibre soporta la conversión de muchos formatos de entrada a muchos formatos de salida. Puede convertir cualquier formato de entrada de la siguiente lista a cualquier formato de salida.

Formatos de entrada: AZW, AZW3, AZW4, CBZ, CBR, CB7, CBC, CHM, DJVU, DOCX, EPUB, FB2, FBZ, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ

Formatos de salida:

Nota

PRC es un formato genérico. calibre admite archivos PRC con encabezados TextRead y MOBIBook. PDB es también un formato genérico. calibre admite archivos PDB eReader, Plucker (sólo entrada), PML y zTxt. La compatibilidad con DJVU es solamente para convertir archivos DJVU que contienen texto incrustado. Éstos están normalmente generados mediante software OCR. Los libros MOBI pueden ser de dos tipos: Mobi6 y KF8. calibre es totalmente compatible con ambos. Los archivos MOBI suelen tener extensiones .azw o .azw3. Se admiten archivos DOCX de Microsoft Word 2007 y posteriores.

¿Cuáles son los mejores formatos de origen para convertir?.

En orden de preferencia decreciente: LIT, MOBI, AZW, EPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

He convertido un archivo PDF, pero el resultado tiene varios problemas.

PDF es un formato terrible como origen de conversión. Puede ver una lista de los distintintos problemas que puede encontrar al convertir archivos PDF en: Convertir documentos PDF.

¿Cómo convierto un archivo que contiene caracteres «exóticos» (acentuados, no latinos, comillas tipográficas, etc.)?

Hay dos aspectos en este problema:

Conocer la codificación del archivo de origen: calibre trata de adivinar qué codificación de caracteres utilizan los archivos de origen, pero a menudo esto es imposible, así que es necesario indicar la codificación que se debe usar. Se puede hacer a través de la interfaz gráfica en el campo Codificación de entrada en la sección Apariencia > Texto del cuadro de diálogo de conversión. Las herramientas de línea de órdenes tienen una opción ebook-convert-txt-input --input-encoding.

Al añadir archivos HTML a calibre, puede ser necesario indicarle la codificación que usan. Para hacerlo, vaya a :gulabel:`Preferencias > Avanzado > Complementos > Tipo de archivo` y personalice el complemento HTML to ZIP, indicando la codificación de los archivos HTML. A partir de ahora al añadir archivos HTML se procesarán correctamente. Los archivos HTML de diferentes fuentes suelen tener codificaciones diferentes, así que puede que sea necesario cambiar la configuración varias veces. Una codificación habitual para archivos de la web es cp1252, así que sugerimos que pruebe con ésta primero. Al convertir archivos HTML, deje el campo de codificación de entrada mencionado anteriormente en blanco. Esto es porque el complemento HTML to Zip convierte automáticamente los HTML añadidos a una codificación estándar (UTF-8).

¿Qué ocurre con los índices de los archivos MOBI?

Lo primero que hay que saber es que la mayoría de los libros electronicos tienen dos índices. El primero es un índice común, tal como se hallaría en un libro de papel. Este índice forma parte del flujo principal del documento, y se puede modificar su formato. A este índice lo llamamos índice de contenido.

También está el índice de metadatos. Un índice de metadatos es un índice que no forma parte del texto del libro y al que se accede normalmente a través de un botón especial en el lector. Por ejemplo, en el visor de libros electrónicos de calibre puede usar el botón Índice para ver este índice. El aspecto de este índice no está especificado por creador del libro, sino que depende del programa de lectura.

En formato MOBI la situación es algo confusa debido a que es el único formato de los habituales para ebooks que no tiene soporte decente para índice de metadatos. Un libro MOBI simula la presencia de un índice de metadatos poniendo un índice de contenido adicional al final del libro. Cuando selecciona «Ir al índice» en un Kindle, éste va dicho índice de contenido adicional.

Puede parecer que el libro MOBI tiene dos índices idénticos. Recuerde que uno es semánticamente el índice de contenido y el otro el de metadatos, a pesar de que ambos tengan las mismas entradas y el mismo aspecto. A uno se puede acceder directamente desde el menú del Kindle, al otro no.

Al convertir a MOBI, calibre detecta el indice de metadatos en el documento de entrada y genera un índice al final de archivo en el MOBI. Puede deshabilitar esta opción en la configuración de salida MOBI. También puede indicar a calibre que lo ponga al principio o al final del libro en la configuración de salida MOBI. Recuerde que este índice es semánticamente un índice de metadatos, en cualquier formato que no sea MOBI no puede ser parte del texto. El hecho de que sea parte el texto en MOBI es accidental debido a las limitaciones de MOBI. Si quiere un índice en un lugar concreto del texto del documento, cree uno a mano. Recomendamos que deje las opciones predeterminadas tal cual están, es decir, con el índice de metadatos al final del libro. También tenga en cuenta que si deshabilita la generación del índice al final del archivo, puede que el MOBI no funcione correctamente en un Kindle, ya que el Kindle usa el índice de metadatos para diversas funciones, incluyendo el paso rápido de páginas.

Si tiene un índice editado a mano en el documento de entrada, puede usar las opciones de detección de índice de calibre para generar automáticamente el índice de metadatos a partir de aquél. Consulte la sección de conversión del Manual de usuario para obtener más detalles sobre cómo usar estas opciones.

Por último, recomendamos eliminar el índice de contenido y mantener únicamente el índice de metadatos en los libros electrónicos. Los índices de metadatos proporcionan a los lectores una experiencia de navegación muy superior (excepto en Kindle, donde son esencialmente iguales a que un índice de contenido).

Nota

El formato AZW3, más reciente, tiene soporte adecuado para el índice de metadatos. Sin embargo, el firmware de Kindle tiende a fallar si se deshabilita la generación del índice al final del archivo. Por lo tanto, se recomienda no tocar el índice generado. Si crea un archivo AZW3 con índice de metadatos sin índice generado al final del archivo, algunas características del Kindle no funcionarán, como el paso rápido de página.

¿Cómo convierto una colección de archivos HTML en un orden específico?

Para convertir una colección de archivos HTML en un orden concreto, debe crear un archivo de índice. Esto es, otro archivo HTML que contenga enlaces a los otros archivos en el orden desado. Dicho archivo tendrá este aspecto:

<html>
   <body>
     <h1>Table of Contents</h1>
     <p style="text-indent:0pt">
        <a href="file1.html">First File</a><br/>
        <a href="file2.html">Second File</a><br/>
        .
        .
        .
     </p>
   </body>
</html>

Entonces, simplemente añada este archivo HTML a la interfaz gráfica y use el botón de Convertir para crear el libro. Puede usar las opciones en la sección Índice del cuadro de diálogo de conversión para controlar cómo se genera el índice.

Nota

De manera predeterminada, al añadir archivos HTML, calibre sigue enlaces en los archivos en el orden de profundidad. Esto quiere decir que si el archivo A.html enlaza a B.html, C.html y D.html, pero B.html también enlaza a D.html, entonces los archivos estarán en el orden A.html, B.html, D.html, C.html. Si por el contrario desea que el orden sea A.html, B.html, C.html, D.html deberá indicar a calibre que los añada en el orden de anchura. Puede hacerlo yendo a Preferencias > Avanzado > Complementos > Tipos de archivo y personalizando el complemento HTML to ZIP.

El EPUB que he creado con calibre no es válido

calibre no garantiza que los EPUB que produzca sean válidos, la única garantía es que si le proporciona XHTML 1.1 + CSS 2.1 válido, producirá un EPUB válido. calibre se esfuerza por asegurar que los EPUB que produce funcionen realmente como deben en una amplia variedad de dispositivos, un objetivo incompatible con producir EPUB válidos, y que es mucho más importante para la gran mayoría de los usuarios. Si necesita una herramienta que siempre produzca EPUB válidos, calibre no es la adecuada. Esto significa que si quiere enviar un EPUB producido por calibre a una tienda online que use un comprobador de validez para EPUB, debe asegurarse de que el EPUB es válido, cosa que calibre no hace. En otras palabras, debe proporcionar a calibre documentos de entrada XHTML + CSS válidos.

¿Cómo puedo utilizar algunas de las funciones avanzadas de las herramientas de conversión?

Puede obtener ayuda sobre cualquier característica individual de los conversores pasando el ratón por encima de ellos en la interfaz gráfica o ejecutando ebook-convert prueba.html .epub -h en una terminal. Un buen punto de partida es el siguiente archivo de demostracion, que muestra algunas de las características avanzadas html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

Integración con dispositivos

Índice

¿Cuáles dispositivos admite calibre?

¿Cómo puedo ayudar para que mi dispositivo sea compatible con calibre?

Mi dispositivo no es detectado por calibre

Mi dispositivo no es estándar o es inusual. ¿Qué puedo hacer para conectar con él?

¿Cómo puedo usar calibre con mi iPad/iPhone/iPod touch?

¿Cómo utilizo calibre con mi teléfono o tableta Android o mi Kindle Fire?

¿Puedo acceder a mis libros en calibre utilizando el navegador de Internet de mi Kindle u otro dispositivo de lectura?

No puedo enviar correos electrónicos usando calibre

Mi dispositivo está montado en Linux como sólo lectura, por lo que calibre no puede conectarse con él.

¿Por qué calibre no es compatible con las colecciones de Kindle o los estantes de Nook?

Me sale un error cuando trato de usar calibre con el Kobo Touch, Glo, etc.

¿Por qué las portadas que envío al Kindle de tinta electrónica aparecen un momento y luego son sustituidas por una portada genérica?

Las portadas de los archivos MOBI han dejado de aparecer en Kindle para PC, Kindle para Android, iPad, etc.

He enviado algunos libros al Kindle con calibre y no aparecen

¿Cuáles dispositivos admite calibre?

calibre puede conectar directamente con todos los lectores de libros electrónicos, smartphones, tablets, etc. principales (y la mayoria de los secundarios). Además, usando la función Conectar a carpeta, puede usar cualqueir lector de libros electrónicos que se muestre como disco USB. Por último, puede establecer una conexión inalámbrica con cualquier dispositivo que tenga un navegador web usando el servidor de contenidos de calibre.

¿Cómo puedo ayudar para que mi dispositivo sea compatible con calibre?

Si el dispositivo aparece como un disco USB en el sistema operativo, es muy fácil añadir soporte en calibre. Sólo necesitamos que nos proporcione cierta información:

Lista completa de los formatos de libro electrónico compatibles con el dispositivo.

¿Hay alguna carpeta especial del dispositivo en el que se deban poner los archivos? ¿El dispositivo detecta archivos en subcarpetas?

También necesitamos información sobre el dispositivo que calibre detectará automáticamente. Primero, si el dispositivo soporta tarjeta SD, insértela. Después conecte el dispositivo al equipo. En calibre vaya a Preferencias > Miscelánea y pulse el botón de Depurar detección de dispositivos. Esto creará una salida de depuración. Copie la salida en un archivo y repita el proceso, esta vez con el dispositivo desconectado del ordenador.

Envíenos ambas salidas mencinadas antes junto con el resto de la información y escribiremos un controlador para su dispositivo.

Una vez que nos envíe la información de salida para un determinado sistema operativo, el soporte para ese dispositivo en ese sistema operativo aparecerá en la siguiente versión de calibre. Para enviarnos dicha información, abra un informe de fallo y envíelo. Ver informar de fallos [https://calibre-ebook.com/es/bugs].

Mi dispositivo no es detectado por calibre

Siga estos pasos para encontrar el problema:

Asegúrese de que está conectando un solo dispositivo cada vez al equipo. No use otro dispositivo soportado por calibre, como un iPhone, iPad, etc. a la vez.

Si conecta un dispositivo Apple (iPad, iPod Touch, iPhone), Apple ya no permite la conexión a través del cable USB a programas de terceros. En lugar de ello, use una conexión inalámbrica a través del servidor de contenidos de calibre.

Si trata de conectar un Kindle Fire u otro dispositivo Android, lea la nota en Con un cable USB.

En macOS, si recibe un error de permiso al conectar un dispositivo a calibre, puede solucionarlo en Preferencias del sistema > Seguridad y privacidad > Privacidad > Archivos y carpetas.

Asegúrese de que está ejecutando la última versión de calibre (actualmente 7.9.0). Siempre se puede descargar la última versión en el sitio de Internet de calibre [https://calibre-ebook.com/es/download]. Puede averiguar qué versión de calibre está ejecutando actualmente examinando la línea inferior de la ventana principal de calibre.

Asegúrese de que el sistema operativo ha detectado el dispositivo. Si es así, el dispositivo debería verse en el Explorador de Windows (en Windows) o en Finder (en macOS).

En calibre, vaya a Preferencias > Dispositivos ignorados y compruebe que el dispositivo no está siendo ignorado

Si todos los pasos anteriores fallan, vaya a Preferencias > Miscelánea, pulse en Depurar detección de dispositivos con el dispositivo conectado y envíe el archivo de salida al registro de errors de calibre [https://bugs.launchpad.net/calibre].

Mi dispositivo no es estándar o es inusual. ¿Qué puedo hacer para conectar con él?

Además de la función Conectar a carpeta, bajo el botón Conectar y compartir, calibre proporciona un complemento de dispositivo User defined, que puede usarse para conectar con cualquier dispositivo USB que aparezca como una unidad de disco en el sistema operativo. Nota: en Windows el dispositivo debe tener una letra de unidad para que calibre pueda usarlo. Vea el dispositivo en Preferencias > Complementos > complementos de Interfaz de dispositivo > User Defined USB driver y Preferencias > Miscelánea > Obtener información para configurar el dispositivo manualmente para más información. Tenga en cuenta que si usa el complemento User defined para un dispositivo que normalmente es detectado por un complemento incorporado de calibre, debe desactivar este último complemento primero, para que pueda usare User defined en su lugar.

¿Cómo puedo usar calibre con mi iPad/iPhone/iPod touch?

Una manera sencilla de explorar la colección de calibre en un dispositivo Apple es utilizando El servidor de contenidos de calibre, que pone la colección disponible sobre la red. Primero lleve a cabo los siguientes pasos en calibre

Establezca el formato de salida principal en EPUB (el formato de salida puede configurarse en Preferencias > Interfaz > Comportamiento)

Establezca el perfil de salida en iPad (también funcionará para iPhone e iPod) en Preferencias > Conversión > Opciones comunes > Configuración de página

Convierta los libros que quiera leer en el dispositivo Apple al formato EPUB seleccionándolos y pulsando el botón Convertir.

Active el servidor de contenido pulsando el botón :guilabel:”Conectar y Compartir” y deje calibre ejecutándose. Puede indicar a calibre que se inicie automáticamente en Preferencias > Compartir > Compartir por la red.

El servidor de contenidos le permite leer libros directamente en Safari. Además, hay muchas aplicaciones para dispositivos Apple que pueden conectar con el servidor de contenidos de calibre. Algunos ejemplos: Marvin, Mapleread e iBooks.

Usar el servidor de contenidos

Inicie el navegador Safari y teclee la dirección IP y el puerto del equipo que está ejecutando el servidor de calibre, de este modo:

http://192.168.1.2:8080/

Sustituya 192.168.1.2 por la dirección IP local del equipo que está ejecutando calibre. Vea El servidor de contenidos de calibre para detalles sobre cómo ejecutar el servidor y encontrar la dirección IP correcta para usar.

Verá una lista de libros en Safari, pulse sobre cualquier libro y se mostrarán opciones para descargarlo o leerlo en el navegador. Si elige descargarlo, Safari le preguntará si quiere abrirlo con iBooks.

Muchas aplicaciones de lectura permiten explorar la biblioteca de calibre directamente mediante el protocol OPDS. En estas aplicaciones puede ir a la pantalla de catálogo en línea y añadir la dirección IP del servidor de calibre para explorar y descargar los libros de la biblioteca de calibre directamente en la aplicación.

¿Cómo utilizo calibre con mi teléfono o tableta Android o mi Kindle Fire?

Hay dos maneras de conectar un dispositivo Android con calibre. Usando un cable USB o inalámbricamente. El primer paso es instalar una aplicación de lectura de libros electrónicos en el dispositivo Android. Existen varias aplicaciones gratuitas y de pagos: Algunos ejemplos (sin ningún orden particular): FBReader [https://play.google.com/store/apps/details?id=org.geometerplus.zlibrary.ui.android&hl=en], Moon+ [https://play.google.com/store/apps/details?id=com.flyersoft.moonreader&hl=en], Mantano [https://play.google.com/store/apps/details?id=com.mantano.reader.android.lite&hl=en], Aldiko [https://play.google.com/store/apps/details?id=com.aldiko.android&hl=en], Kindle [https://play.google.com/store/apps/details?id=com.amazon.kindle&feature=related_apps].

Con un cable USB

Simplemente conecte el dispositivo al equipo con un cable USB. calibre detectará el dispositivo automáticamente y entonces podrá transferirle libros pulsando el botón Enviar al dispositivo. Tenga en cuenta que en macOS y Linux un único programa puede conectarse con un dispositivo Android simultáneamente, así que asegúrese de que el dispositivo no esté abierto por el Administrador de archivos OS, la utilidad de transferencia de archivos Android, etc.

Nota

Con dispositivos Android más recientes, puede que tenga que hacer algunas acrobacias para conseguir que funcione la conexión, pues Google no tiene interés en facilitar la independencia de su «nube». Primero, desbloquee la pantalla antes de conectar el cable USB. Cuando conecte el cable USB aparecerá una notificación. Asegúrese de que dice algo como «Transferir archivos de medios» o «MTP (Media Transfer mode)». Si no es así, toque la notificación y cambie el modo a transferencia de medios (MTP). Puede que tenga que reiniciar calibre en este momento para que se reconozca el dispositivo. Finalmente, puede que reciba una notificación en el dispositivo cada vez que calibre o el sistema operativo intente realmente conectar con el dispositivo, pidiendo permiso; si es así, acepte.

Nota

Con el Kindle Fire 8 o más reciente hay un icono que aparece cuando se conecta el cable USB, indicando que el dispositivo se está cargando. Toque el icono, cambie el dispositivo a modo de transferencia de datos y reinicie calibre, ahora debería detectarlo.

Por la red

calibre cuenta con un servidor web incorporado, El servidor de contenidos de calibre. Hace que toda la colección de calibre pueda estar disponible en red. Puede explorarla en un dispositivo usando un navegador corriente o una apliciación específica. Primero realice los siguientes pasos en calibre:

Establezca el Formato de salida principal en calibre como EPUB para dispositivos Android normales o MOBI para los Kindle. (El formato de salida puede establecerse en Preferencias > Interfaz > Comportamiento.)

Convierta los libros que quiera leer en el dispositivo Apple al formato EPUB o MOBI seleccionándolos y pulsando el botón Convertir.

Active el Servidor de contenido en las preferencias de calibre y deje calibre funcionando.

Ahora, en el dispositivo Android, abra el navegador y vaya a

http://192.168.1.2:8080/

Sustituya 192.168.1.2 por la dirección IP local del equipo que está ejecutando calibre. Vea El servidor de contenidos de calibre para detalles sobre cómo ejecutar el servidor y encontrar la dirección IP correcta para usar.

Ahora podrá explorar la colección de libros y descargarlos desde calibre al dispositivo para abrirlo con el programa de lectura de libros que tenga en el dispositivo Android.

Muchas aplicaciones de lectura permiten explorar la biblioteca de calibre directamente mediante el protocol OPDS. En estas aplicaciones puede ir a la pantalla de catálogo en línea y añadir la dirección IP del servidor de calibre para explorar y descargar los libros de la biblioteca de calibre directamente en la aplicación.

¿Puedo acceder a mis libros en calibre utilizando el navegador de Internet de mi Kindle u otro dispositivo de lectura?

calibre tiene un servidor de contenidos que exporta los libros de calibre como una página web. Vea El servidor de contenidos de calibre para más detalles.

Algunos dispositivos, como el Kindle (1/2/DX), no permiten acceder al puerto 8080 (el puerto predeterminado donde se ejecuta el servidorde contenidos). En ese caso, cambia el puerto en las Preferencias de calibre a 80. (En algunos sistemas operativos, no se puede ejecutar el servidor en un puerto con un número menor de 1024 debido a las configuraciones de seguridad. En ese caso, la solución más simple es ajustar el enrutador para redirigir las solicitudes del puerto 80 al 8080).

Además, algunos dispositivos no tienen navegadores suficiente avanzados para ejecutar la interfaz que emplea el servidor de contenidos. En estos dispositivo, puede añadir /mobile al URL del servidor para acceder a una interfaz simplificada sin JavaScript.

No puedo enviar correos electrónicos usando calibre

Debido a la vasta cantidad de correo basura en el correo electrónico, enviar correos puede resultar complicado, ya que los servidores de correo utilizan diferentes estrategias para bloquear correos electrónicos. El problema más común ocurre si envía el correo directamente desde calibre, sin usar un servidor de repetición. Muchos servidores (por ejemplo Amazon) bloquean el correo electrónico que no proviene de un repetidor reconocido. La manera más sólida de configurar el envío de correo electrónico en calibre es la siguiente:

Cree una cuenta de GMX gratuita en GMX [https://www.gmx.com].

Vaya a Preferencias > Compartir > Compartir libros por correo electrónico en calibre, pulse el botón Usar GMX y complete la información requerida.

Inicie la sesión en la cuenta de GMX en la página web y active el envío STMP (Settings > POP3 & IMAP > Send and receive emails via external program)

calibre podrá entonces usar GMX para enviar el correo electrónico.

Si está enviando a un Kindle, recuerde actualizar las preferencias de correo electrónico en su página de Amazon Kindle para permitir correos enviados desde su dirección de correo GMX. Además tenga en cuenta que Amazon no permite la entrega por correo electrónico de archivos AZW3 o MOBI de nuevo estilo (KF8). Por último, Amazon ha comenzado recientemente a enviar mensajes de confirmación a la cuenta GMX en los que debe pulsar antes de que el libro se envíe.

Incluso después de hacer esto, puede tener problemas. Una fuente de problemas común es que algunos programas de antivirus mal diseñados impiden que calibre pueda abrir una conexión para enviar corro electrónico. Pruebe a añadir una exclusión para calibre en el programa antivirus.

Nota

Microsoft o GMX pueden desactivar una cuenta si se usa para enviar gran cantidad de correos. Por lo tanto, al usar estos servicios para enviar correos calibre se limita a enviar un libro cada cinco minutos. Si no tiene inconveniente en arriesgarse a que su cuenta sea bloqueada, puede reducir este intervalo en Preferencias > Avanzado > Ajustes en calibre.

Nota

Recientemente Google ha estropeado el soporte de envío de correos electrónicos (SMTP) para forzar el uso de la interfaz web y así poder mostrar más publicidad. Aseguran que el protocolo SMTP es inseguro, lo cual es incorrecto y no es más que una excusa. Use algún otro proveedor de correo electrónico en su lugar.

Nota

Si le preocupa otorgar a calibre acceso a su su cuenta de correo electrónico, cree una nueva cuenta gratuita con GMX o Outlook y úsela únicamente para calibre.

Mi dispositivo está montado en Linux como sólo lectura, por lo que calibre no puede conectarse con él.

Los núcleos de Linux montan los dispositivos en modo sólo lectura cuando sus sistemas de archivos tienen errores. Puede reparar el sistema de archivos con:

sudo fsck.vfat -y /dev/sdc

Sustituya /dev/sdc por la ruta de acceso al nodo del dispositivo. Puede encontrar el nodo, que siempre estará bajo /dev, examinando la información de salida de:

mount

¿Por qué calibre no es compatible con las colecciones de Kindle o los estantes de Nook?

Ni el Kindle ni el Nook proporcionan ninguna manera de manipular colecciones mediante una conexión USB. Si realmente quiere usar colecciones, le recomendaría que vendiera su Kindle o Nook y se hiciera de un Kobo. Sólo Kobo parece entender que la vida es demasiado corta para introducir las colecciones una a una en la pantalla del dispositivo 😇

Tenga en cuenta que caso del Kindle hay una manera de gestionar colecciones a través de USB, pero requiere reiniciar el Kindle cada vez que se desconecta del equipo para que se reconozcan los cambios en las colecciones. Mientras sea así, es improvable que ningún desarrollador de calibre se sienta motivado para ofrecer soporte. Hay, no obstante, un complemento de calibre que le permite crear colecciones en el Kindle a partir de los metadatos de calibre. Está disponible aquí [https://www.mobileread.com/forums/showthread.php?t=244202].

Nota

Amazon ha eliminado completamente la posibilidad de manipular colecciones en sus modelos más recientes, como el Kindle Touch y el Kindle Fire, haciendo que incluso el complemento anterior sea totalmente inútil, a menos que modifique su Kindle e instale un firmware personalizado.

Me sale un error cuando trato de usar calibre con el Kobo Touch, Glo, etc.

El Kobo tiene un firmware muy defectuoso. Se sabe que la conexión puede fallar aleatoriamente. Algunas combinaciones de placa madre y puertos, cables o concentradores USB pueden aumentar esta tendencia a los fallos. Si observa un error al conectar el dispositivo con calibre, pruebe las siguientes sugerencias, cada una de las cuales ha resuelto el problema para algunos usuarios de calibre.

Conecte el Kobo directamente al equipo, sin utilizar un hub USB.

Pruebe un cable USB diferente y un puerto USB diferente en el equipo

Cierre la sesión en el Kobo e iníciela de nuevo, esto hace que se reconstruya la base de datos, eliminando los errores de base de datos corrupta.

Intente actualizar el firmware del Kobo Touch a la última versión

Intente reiniciar el Kobo (algunas veces esta acción resuelve el problema por un tiempo, pero luego vuelve a aparecer, y en tal caso debe reiniciarlo una y otra vez)

Pruebe a copiar uno o dos libros al Kobo de cada vez y no mantenga colecciones muy grandes en él.

¿Por qué las portadas que envío al Kindle de tinta electrónica aparecen un momento y luego son sustituidas por una portada genérica?

Esto ocurre debido a un fallo de Amazon. Intentan descargar una portada para el libro de sus servidores y, cuando esto falla, sustituyen la portada que calibre ha creado por una portada genérica. Para más detalles ver este hilo del foro [https://www.mobileread.com/forums/showthread.php?t=329945] (en inglés). En la versión 4.17, calibre intenta solucionar el problema: si vuelve a conectar el Kindle a calibre después de que Amazon haya destruido las portadas, calibre las restaurará automáticamente. Por lo tanto, para poder ver las portadas en el Kindle tendrá que:

Enviar el libro al Kindle con calibre

Desconectar el Kindle y esperar a que Amazon destruya la portada

Volver a conectar el Kindle con calibre

Tenga en cuenta que esta solución sólo funciona para libros enviados por calibre 4.17 o posterior. Otra opción es mantener el Kindle en modo avión, no es necesario que Amazon sepa qué libros lee, de todas formas. Le animo a que contacte con el servicio de atención al cliente Amazon y se queje firmemente de este fallo. Quizá en Amazon hagan caso.

Nota

Si esta solución alternativa no le funciona, asegúrese de que la versión de firmware del Kindle es al menos 5.12.5, publicada en abril de 2020.

Las portadas de los archivos MOBI han dejado de aparecer en Kindle para PC, Kindle para Android, iPad, etc.

Esto se debe a un error en el software de Amazon. Puede evitarlo yendo a Preferencias > Conversión > Opciones de salida > Salida MOBI y seleccionando la opción Activar el intercambio del contenido del libro. Si está reconvirtiendo un libro anteriormente convertido, también deberá habilitar la opción del diálogo de conversión para ese libro concreto (puesto que las configuraciones de conversión libro a libro se guardan y tienen preferencia).

Tenga en cuenta esto hará que el MOBI generado aparezca en «Documentos personales» en vez de en «Libros» en el Kindle Fire, y la sincronización de Amazon no funcionará, pero las portadas sí. Debe elegir qué funcionalidad es más importante. Recomendamos contactar con Amazon y pedirles que arreglen este error.

El fallo en el software de Amazon es que cuando se pone un archivo MOBI en un Kindle, a menos que el archivo esté marcado como documento personal, presupone que el libro ha sido comprado en Amazon e intenta descargar la miniatura de portada de su servidor. Cuando la descarga falla, no toma la portada definida en el archivo MOBI. Esto es posiblemente un acto deliberado por parte de Amazon para forzar a los autores a vender a través de su portal. En otras palabras. El Kindle sólo muestra portadas para libros marcados como documentos personales o libros comprados directamente de Amazon.

Si envía un archivo MOBI a un Kindle de tinta electrónica con calibre usando una conexión USB, calibre evita este problema enviando él mismo la miniatura de portada. Sin embargo, esta solución sólo es posible si se usa una conexión USB y se envía el libro con calibre. Tenga en cuenta que si envía el libro mediante correo electrónico, Amazon marcará el archivo MOBI automáticamente como documento personal y la portada funcionará, pero el libro aparecerá en los documentos personales.

He enviado algunos libros al Kindle con calibre y no aparecen

Los libros enviados al Kindle sólo aparecen en el Kindle una vez que éste los ha indexado. Esto puede llevar algún tiempo. Si el libro sigue sin aparecer al cabo de unos minutos, es posible que el indexador del Kindle hay fallado. A veces un libro particular puede hacer que el indexador falle. Por desgracia, Amazon no proporciona ninguna forma de saber qué libro causa el fallo. El único recurse es reiniciar el Kindle o borrar todos los archivos de su memoria con el explorador de Windows (o el administrador de archivos que utilice) y volver a enviar los libros, uno a uno, hasta descubrir el libro problemático. Una vez localizado el libro, bórrelo del Kindle, haga una conversión de MOBI a MOBI o de MOBI a AZW3 en calibre y envíelo de nuevo. Muy probablemente esto solucione el problema.

Gestión de la biblioteca

Índice

¿Dónde se almacenan los archivos de los libros?

¿Cómo gestiona calibre los nombres de autor y su orden?

¿Por qué calibre no me deja guardar mis libros en mi propia estructura de carpetas?

¿Por qué calibre no tiene una columna para tal o cual cosa?

¿Puedo tener una columna que muestre los formatos o el ISBN?

¿Cómo muevo mis datos de calibre de un equipo a otro?

¡La lista de libros en calibre está vacía!

Estoy sufriendo errores con la biblioteca calibre en una unidad de red o NAS

¿Dónde se almacenan los archivos de los libros?

La primera vez que ejecute calibre, le va a preguntar por la carpeta dande quiere guardar los libros. Cada vez que añada un libro a calibre, se copiará el libro en dicha carpeta. En la carpeta se organizaran muy bien los libros en subcarpetas por autory título. Tenga en cuenta que los contenidos de esta carpeta serán gestionados por automáticamente por calibre, no añada archivos o carpetas manualmente en esta carpeta, porque pueden ser automaticamente eliminados. Si quiere añadir un archivo asociado a un libro en particular, use el área superior derecha del cuadro Modificar metadatos para hacerlo. Entonces calibre colocará automáticamente el archivo en la carpeta correcta y lo moverá cuándo el título o autor cambien.

Los metadatos de los libros se almacena en el archivo metadata.db en el nivel superior de la carpeta de la biblioteca. Este archivo es una base de datos de sqlite. Al realizar copias de seguridad de la biblioteca asegúrese de copiar toda la carpeta y todas sus subcarpetas.

La carpeta de la biblioteca y todo su contenido conforman lo que llamamos biblioteca de calibre. Puede tener varias bibliotecas. Para gestionar las bibliotecas, pulse en el icono de calibre en la barra de herramientas. Puede crear nuevas bibliotecas, eliminar o cambiar el nombre de las existentes y cambiar de una biblioteca a otra fácilmente.

Puede copiar o mover libros entre diferentes bibliotecas (una vez que haya configurado más de una biblioteca) pulsando con el botón derecho sobre un libro y seleccionando la acción Copiar a biblioteca.

¿Cómo gestiona calibre los nombres de autor y su orden?

Los nombres de los autores son complejos, especialmente si se consideran diferentes culturas, vea esta nota [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en] (en inglés) sobre algunas de estas complejidades. calibre tiene una estrategia muy flexible para gestionar los nombres autor. Lo primero que hay que entender es que los libros y los autores son entidades separadas en calibre. Un libro puede tener más de un autor, y un autor puede tener más de un libro. Puede gestionar autores individuales pulsando con el botón derecho sobre el autor en el explorador de etiquetas a la izquierda de la ventana principal de calibre y seleccionando Administrar autores. Mediante este cuadro de diálogo puede modificar el nombre de un autor o cómo se ordena éste. Si lo hace, se cambiará automáticamente el nombre del autor en todos los libros de dicho autor. Cuando un libro tiene múltiples autores, separe sus nombres usando el carácter &.

En cuanto a la ordenación de nombres de autor:

Cuando se añade un nuevo autor a calibre (esto ocurre cuando se añade un libro con un nuevo autor), calibre calcula automáticamente un texto de orden para el libro y para el autor.

Los autores en el explorador de etiquetas se ordenan por el valor de orden de los autores. Recuerde que esto puede ser distinto del campo «orden de autor» de un libro.

De manera predeterminada, este algoritmo de ordenación asume que el nombre del autor está en el formato Nombre Apellido y genera un valor con el formato Apellido, Nombre.

Puede cambiar este algoritmo en Preferencias > Avanzado > Ajustes, seleccionando el ajuste author_sort_copy_method.

Puede forzar que calibre vuelva a calcular los valores de orden de autor para cada autor pulsando con el botón derecho sobre cualquier autor y seleccionando Administrar autores, a continuación, pulse el botón Recalcular todos los valores de orden de autor . Haga esto después de haber establecido el ajuste autor_sort_copy_method como desee.

Puede hacer que calibre recalcule los valores de orden de autor de todos los libros utilizando el cuadro de diálogo de edición masiva de metadatos (seleccione todos los libros y pulse en modificar metadatos, marque la casilla Establecer automáticamente el orden de autor, después pulse Aceptar.)

Al volver a calcular los valores de orden de autor para los libros, calibre utiliza los valores de orden de autor de cada autor individual. Por lo tanto, asegúrese de que los valores de orden de autor individuales son correctos antes de calcular los valores de orden de autor de los libros.

Puede controlar si el explorador de etiquetas muestra los autores usando sus nombres o sus valores de ordenación estableciendo el ajuste categories_use_field_for_author_name en Preferencias > Avanzado > Ajustes.

Tenga en cuenta que puede establecer valores de orden de autor individuales como quiera usando Administrar autores. Esto es útil cuando se trata de nombres que calibre no maneja correctamente, como nombres con varias partes complejas como «Miguel de Cervantes Saavedra» o de nombres asiáticos como «Sun Tzu».

Con toda esta flexibilidad, es posible hacer que calibre administre los nombres de autores como más le guste. Por ejemplo, una solicitud común es mostrar nombres como «apellido, nombre». Para ello, y si la nota siguiente no se aplica en su caso:

Fije author_sort_copy_method en copy como se describe arriba.

Reinicie calibre. No cambie ningún metadato de los libros antes de llevar a cabo los siguientes pasos.

Cambie todos los nombres de autor a «Apellido, Nombre» utilizando el cuadro de diálogo de Administrar autores.

Después de haber cambiado todos los autores, pulse el botón Recalcular todos los valores de orden de autor.

Pulse Aceptar, calibre cambiará los autors de todos los libros. Esto puede llevar un rato.

Nota

Al cambiar de Nombre Apellido a Apellido, Nombre, a menudo ocurre que los valores de author_sort ya están en el formato Apellido, Nombre. Si este es el caso, haga lo siguiente:

Fije author_sort_copy_method en copy como se describe arriba.

Reinicie calibre. No cambie ningún metadato de los libros antes de llevar a cabo los siguientes pasos.

Abra el cuadro de diálogo «Administrar autores». Pulse el botón Copiar todos valores de orden de autor a autor.

Revise los autores para asegurarse de que está satisfecho. Aún puede pulsar Cancelar para abandonar los cambios. Una vez que presione Aceptar, no hay posibilidad de deshacer.

Pulse Aceptar, calibre cambiará los autors de todos los libros. Esto puede llevar un rato.

¿Por qué calibre no me deja guardar mis libros en mi propia estructura de carpetas?

La ventaja de las funciones de administración de biblioteca en calibre es que proporcionan una interfaz de búsqueda y ordenación para encontrar libros que es mucho más eficiente que cualquier esquema de carpetas que pueda pensarse para la colección. De hecho, una vez que se sienta cómodo usando la interfaz de calibre para buscar, clasificar y explorar la colección, no sentirá de nuevo la necesidad de buscar por los archivos en el disco para encontrar un libro. Mediante la gestión de los libros en su propia estructura de carpetas Autor > Título > Archivos del libro, calibre es capaz de lograr un alto nivel de fiabilidad y estandarización. Para ilustrar por qué una interfaz basada en la búsqueda y etiquetado es superior a las carpetas, considere lo siguiente. Supongamos que su colección de libros está muy bien ordenada en carpetas con el siguiente esquema:

Genre -> Author -> Series -> ReadStatus

Esto hace que sea muy fácil encontrar, por ejemplo, todos los libros de ciencia ficción de Isaac Asimov en la serie Fundación. Pero supongamos que quiere encontrar todos los libros de ciencia ficción no leídos. No hay manera fácil de hacer esto con este esquema de carpetas, tendría que tener un esquema de carpetas similar a:

ReadStatus -> Genre -> Author -> Series

En calibre, en lugar de carpetas utilizaría etiquetas para marcar el género y el estado de lectura, y luego sólo hay que utilizar una consulta de búsqueda como tag:"ciencia ficción" and not tag:leído. calibre tiene incluso una interfaz gráfica, por lo que no necesita aprender su lenguaje de búsqueda, sino que puede pulsar sobre las etiquetas para incluirlas o excluirlas de la búsqueda.

Frente a quienes dicen que es necesario tener acceso al sistema de archivos para poder acceder a los libros en una red, calibre cuenta con un servidor de contenido excelente que proporciona acceso a la biblioteca de calibre a través de la red.

Si le preocupa que algún día calibre deje de desarrollarse, dejando todos sus libros abandonados en su estructura de carpetas, examine la potente función de calibre Guardar a disco, que le permite exportar todos los archivos en una estructura de carpetas de complejidad arbitraria, basada en sus metadatos.

Por último, la razón de que haya números al final del titulo de cada carpeta es por robustez del sistema. Ese número es el número de identificación del registro del libro en la base de datos de calibre. La presencia del número le permite tener múltiples registros con los mismos nombres de título y autor. También es la parte que permite a calibre regenerar mágicamente la base de datos con todos los metadatos si el archivo de base de datos se corrompe. Dado que la misión de calibre es conseguir que deje de almacenar metadatos en los nombres de archivo y de usar el sistema de archivos para encontrar las cosas, el aumento de la robustez que ofrecen los números de identificación vale la pena a pesar de que los nombres de carpeta sean más feos.

Si aún no está convencido, me temo que calibre no es para Ud. Busque en otros lugares lo que necesite para catalogar libros. Para ser claros: esto no va cambiar. Por favor no contacte con nosotros para hacernos cambiarlo.

¿Por qué calibre no tiene una columna para tal o cual cosa?

calibre está diseñado con columnas para los campos más frecuentes y extendidos. Además, puede añadir cualquier otra columna que desee. Las columnas pueden añadirse en Preferencias > Interfaz > Añadir columnas personalizadas. Vea el cursillo UI Power tips [https://calibre-ebook.com/es/demo#tutorials] (en inglés) para aprender a crear columnas personalizadas, o lea esta entrada de blog [https://blog.calibre-ebook.com/calibre-custom-columns.html] (en inglés).

También puede crear «columnas virtuales» que contienen combinaciones de los metadatos de otras columnas. En el cuadro de añadir columna, use los enlaces de Creación rápida para crear columnas que muestren el ISBN del libro o los formatos. Puede usar el potente lenguaje de plantillas de calibre para hacer mucho más con las columnas. Para más detalles, véase El lenguaje de plantillas de calibre.

¿Puedo tener una columna que muestre los formatos o el ISBN?

Sí, puede. Siga las instrucciones en la respuesta anterior para añadir columnas personalizadas.

¿Cómo muevo mis datos de calibre de un equipo a otro?

Puede exportar todos los datos de calibre (libros, configuraciones y complementos) y luego importarlos en otro equipo. Veamos primero cómo exportar los datos:

Pulse con el botón derecho sobre el icono de calibre en la barra de herramientas principal de calibre y seleccione Exportar o importar todos los datos de calibre. Tenga en cuenta que si hay un dispositivo conectado, esta opción de menú no estará disponible, así que desconecte los dispositivos. Luego pulse el botón llamado Exportar todos los datos de calibre. Verá una lista de todas las bibliotecas de calibre. Pulse «Aceptar» y elija una carpeta vacía en algún lugar del equipo. Los datos exportados se guardarán en esta carpeta. Copie esta carpeta al nuevo equipo y siga las instrucciones siguientes para importar los datos.

Instale calibre en el nuevo equipo y pase el :guilabel:Asistente de bienvenida`, no importa lo que haga aquí, ya que a continuación importará la configuración antigua. Ahora tiene un calibre vacío, con sólo la guía Getting Started en la biblioteca. Nuevamente, pulse con el botón derecho sobre el botón de calibre y elija Exportar o importar todos los datos de calibre. Luego pulse el botón Importar datos previamente exportados. Seleccione la carpeta con los datos exportados que ha copiado anteriormente. Verá ahora una lista de las bibliotecas que puede importar. Una a una, seleccione para cada biblioteca una nueva ubicación (una ubicación no es más que una carpeta vacía en algún lugar del equipo). Pulse «Aceptar». Una vez finalice la importación, calibre se reiniciará con todas las antiguas bibliotecas, configuraciones y complementos de calibre.

Nota

Esta función de exportación e importación sólo está disponible a partir de la versión de calibre 2.47. Si tiene una versión anterior de calibre, o si experimenta problemas al exportar o importar, puede simplemente copiar la carpeta de calibre manualmente, como se describe en el párrafo siguiente.

Basta con copiar la carpeta de la biblioteca de calibre del antiguo equipo al nuevo. Puede averiguar cuál es la carpeta de la biblioteca pulsando sobre el icono de calibre en la barra de herramientas. Elija la acción Cambiar o crear biblioteca y verá la ruta a la biblioteca de calibre actual.

Después, en el nuevo equipo, inicie calibre por primera vez. Se ejecutará el Asistente de bienvenida, que le pedirá la ubicación de la biblioteca de calibre. Señale la carpeta previamente copiada. Si el equipo al que va a transferir la biblioteca ya cuenta con una instalación de calibre, el Asistente de bienvenida no se ejecutará. En ese caso, pulse con el botón derecho sobre el icono de calibre en la barra de herramientas y diríjalo a la carpeta recién copiada. Ahora tendrá dos bibliotecas de calibre en el equipo y podrá cambiar entre ellas pulsando sobre el icono de calibre en la barra de herramientas. Al transferir la biblioteca de esta manera se conservan todos los metadatos, etiquetas, columnas personalizadas, etc.

¡La lista de libros en calibre está vacía!

Para poder comprender por qué ocurre esto, debe entender lo que es una biblioteca de calibre. Al nivel más básico, una biblioteca de calibre es sólo una carpeta. Cada vez que añade un libro a calibre, los archivos de ese libro se copian en esta carpeta (dividida en subcarpetas por autor y título). Dentro de la carpeta de biblioteca de calibre, en el nivel superior, encontrará un archivo llamado metadata.db. En este archivo es donde calibre almacena los metadatos como título, autor, calificación, etiquetas, etc. para todos los libros de la biblioteca. La lista de los libros que muestra calibre se crea a partir del contenido de este archivo metadata.db.

Hay dos razones por las que calibre puede mostrar una lista de libros vacía:

La carpeta de la biblioteca de calibre ha cambiado de ubicación. Esto puede suceder si estaba en un disco externo y la letra de unidad del disco ha cambiado, o si ha movido la carpeta accidentalmente. En este caso, calibre no puede encontrar la biblioteca y, por lo tanto, se inicia con una biblioteca vacía. Para remediar esto, pulse con el botón derecho sobre el botón de biblioteca de la barra de herramientas de calibre y seleccione Cambiar o crear biblioteca. Pulse en el pequeño icono azul para seleccionar la nueva ubicación de la biblioteca de calibre y pulse Aceptar. Si no conoce la nueva ubicación, busque el archivo metadata.db en el equipo.

El archivo metadata.db ha sido borrado o dañado. En este caso, puede pedir a calibre que reconstruya metadata.db a partir de las copias de seguridad. Pulse con el botón derecho sobre el icono de calibre en la barra de herramientas de calibre y seleccione Mantenimiento de la biblioteca > Restaurar base de datos. calibre reconstruirá automáticamente metadata.db.

Estoy sufriendo errores con la biblioteca calibre en una unidad de red o NAS

No ubique la biblioteca de calibre en una unidad de red.

Un sistema de archivos es un asunto complicado. La mayoría de los sistemas de archivos en red carecen de varias de las características que usa calibre. Algunos no admiten bloqueo de archivos, algunos no admiten enlaces duros, algunos son simplemente inestables. Además, calibre es una aplicación de un solo usuario. Si ejecuta dos copias de calibre inadvertidamente sobre la misma biblioteca en red, pueden ocurrir cosas malas. Por último, los diferentes sistemas operativos imponen distintas restricciones sobre los sistemas de archivos, por lo que, de nuevo, si comparte una unidad en red entre distintos sistemas operativos, ocurrirán cosas malas.

Considere utilizar el servidor de contenido de calibre para hacer que sus libros estén disponibles en otros equipos. Ejecute calibre en un único equipo y acceda a él a través del servidor de contenido o de una solución de escritorio remoto.

Si tiene que compartir la biblioteca real, utilice una herramienta de sincronización de archivos como DropBox o rsync en vez de una unidad de red. Si está utilizando una herramienta de sincronización de archivos es esencial que se asegure de que calibre y la herramienta de sincronización de archivos no tratan de acceder a la biblioteca de calibre al mismo tiempo. En otras palabras, no ejecute la herramienta de sincronización de archivos y calibre al mismo tiempo.

Incluso con estas herramientas hay riesgo de corrupción o pérdida de datos, por lo que sólo debe hacer esto si está dispuesto a asumir ese riesgo. En particular, tenga en cuenta que Google Drive es incompatible con calibre, si pone la biblioteca de calibre en Google Drive, sufrirá de pérdida de datos. Véase este hilo [https://www.mobileread.com/forums/showthread.php?t=205581] (en inglés) para más detalles.

Miscelánea

Índice

¿Amazon va a interrumpir el envío por correo electrónico de archivos MOBI?

Quiero que calibre descargue noticias de mi sitio de noticias favorito.

¿Por qué el programa se llama «calibre»?

¿Por qué calibre no muestra todos los tipos de letra de macOS?

calibre no se inicia en Windows

calibre se bloquea o cierra ocasionalmente

Ni el visor ni la herramienta de modificación de libros electrónicos de calibre funcionan e Windows

Al usar el visor o realizar una conversión aparece un error de permiso denegado en Windows.

calibre no se inicia o se detiene en macOS

Sólo veo una pantalla negra o blanca al ejecutar el visor de libros electrónicos de calibre

He descargado el instalador, pero no funciona.

Mi programa antivirus dice que calibre es un virus o un troyano

¿Cómo hago copias de respaldo de calibre?

¿Cómo uso en calibre los libros EPUB comprados (o qué hago con los archivos .acsm)?

Me sale un error de «Permiso denegado»

¿Puedo hacer que los metadatos de comentario se muestren en el lector?

¿Cómo hago que calibre utilice mi proxy HTTP?

Quiero que se añada una función a calibre. ¿Qué puedo hacer?

¿Por qué calibre no se actualiza automáticamente?

¿Qué licencia tiene calibre?

¿Cómo ejecuto calibre desde una memoria USB?

¿Cómo ejecuto partes de calibre como la descarga de noticias o el servidor de contenido en mi propio servidor Linux?

¿Amazon va a interrumpir el envío por correo electrónico de archivos MOBI?

Amazon ha anunciado [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/] que va a dejar de aceptar archivos MOBI enviados a las direcciones @kindle.com. Puede hacer que calibre envíe EPUB en lugar de MOBI en Preferencias > Compartir libros por correo electrónico, eliminando MOBI de la lista de formatos para enviar a la dirección @kindle.com y añadiendo EPUB en su lugar.

No obstante, tenga en cuenta que la admisión de EPUB por parte de Amazon es muy deficiente y rechazará archivos EPUB que funcionan en cualquier otro sitio. En tales casos puede intentar el siguiente truco:

Convertir el archivo EPUB a MOBI

Después convertir el archivo MOBI de nuevo a EPUB y enviar el archivo EPUB resultante

Esto eliminará todo el formato avanzado, tipos de letra incrustados, etc., pero aumentará enormemente la probabilidad de que Amazon acepte el EPUB.

Nota

Si estaba usando el envío por correo electrónico de publicaciones periódicas descargadas por calibre, será mejor que las envíe por cable USB o que las descargue del servidor de contenidos de calibre a través del navegador del Kindle. Sin embargo, si quiere continuar usando el envío por correo electrónico puede probar a cambiar el formato de salida en Preferencias > Comportamiento a EPUB, entonces calibre descargará las noticias en formato EPUB. Que Amazon acepte o no el EPUB es otra cuestión.

Quiero que calibre descargue noticias de mi sitio de noticias favorito.

Si se maneja bien con la informática, puede hacer que calibre descargue noticias de cualquier sitio web de su elección. Para aprender cómo hacerlo, vea Añadir su sitio de noticias favorito.

O puede solicitar un sitio de noticias particular escribiendo en el foro de calibre Recipes [https://www.mobileread.com/forums/forumdisplay.php?f=228] (en inglés).

¿Por qué el programa se llama «calibre»?

Elija lo que prefiera:

Converter And LIBRary for E-books

Un producto de alto calibre

Un tributo al SONY Librie, que fue el primer lector de libros electrónicos con tecnología de «e-ink»

Mi esposa lo eligió ;-)

En inglés, calibre se pronuncia cal-i-ber, no ca-li-bre. Si se pregunta el motivo, «calibre» es la ortografía británica/commonwealth de «caliber». Al ser indio, es para mí la forma natural de escribirlo.

¿Por qué calibre no muestra todos los tipos de letra de macOS?

calibre incrusta tipos de letra en los archivos que crea. Los archivos de libro electrónico sólo admiten la incrustación de tipos de letra TrueType y OpenType (.ttf y .otf). La mayoría de los tipos de letra en sistemas macOS están en el formato .dfont, por lo que no pueden ser incrustados. calibre sólo muestra los tipos de letra TrueType y OpenType que encuentra en el sistema. Puede obtener muchos tipos de letra en Internet. Simplemente descargue los archivos .ttf u .otf y añádalos a la carpeta Library/Fonts en su carpeta base.

calibre no se inicia en Windows

Puede haber varias causas para esto:

Si no recibe ningún error pero la ventana de calibre no aparece, probablemente haya aparecido fuera de la pantalla. Puede reunir todas las ventanas en la pantalla actual utilizando una de las técnicas descritas aquí [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows] (en inglés).

Si obtiene un error acerca de que calibre no puede abrir un archivo debido a que está siendo utilizado por otro programa, haga lo siguiente:

Desinstale calibre

Reinicie el equipo

Reinstale calibre. Pero no inicie calibre desde el asistente de instalación.

Desactive temporalmente el programa antivirus (desconecte de Internet antes de hacerlo, por seguridad)

Mire en la carpeta que ha elegido para la biblioteca de calibre. Si hay un archivo llamado metadata.db, bórrelo.

Inicie calibre

A partir de ahora debería poder iniciar calibre normalmente.

Si obtiene un error acerca de una función Python que finaliza inesperadamente después de actualizar calibre, primero desinstale calibre, después borre las carpetas (si existen) C:\Program Files\Calibre y C:\Program Files\Calibre2. Ahora reinstale y todo debería funcionar.

Si se produce un error en el Asistente de bienvenida o en la ejecución inicial de calibre, puebe a elegir una carpeta como C:\biblioteca como la biblioteca de calibre (a veces calibre tiene problemas con ubicaciones de biblioteca si la ruta contiene caracteres no ingleses, o sólo números, etc.)

Pruebe a ejecutarlo como administrador (pulse con el botón derecho sobre el icono, y elija Ejecutar como administrador)

Si todavía no comienza, inicie la terminal (presione la tecla de Windows y R; a continuación teclee cmd.exe en el cuadro de diálogo que aparece). En el símbolo del sistema, escriba la siguiente orden y pulse Intro:

calibre-debug -g

Comparta cualquier información que aparezca en un mensaje de ayuda en el Foro [https://www.mobileread.com/forums/forumdisplay.php?f=166] (en inglés).

calibre se bloquea o cierra ocasionalmente

Conozco varias cosas que pueden causar esto:

Ha conectado recientemente un monitor externo o un televisor al equipo. En este caso, cada vez que calibre abre una nueva ventana, como la ventana de modificar metadatos o el cuadro de diálogo de conversión, aparecerá en el segundo monitor, donde no la ve y puede pensar que calibre se ha colgado. Desconecte el segundo monitor y reinicie calibre.

Se sabe que los siguientes programas pueden causar errores en calibre, si está ejecuntando alguno de ellos, ciérrelo antes de iniciar calibre o desinstálelo: RoboForm, Logitech SetPoint Settings, Constant Guard Protection by Xfinity, Spybot, Killer Network Manager, Nahimic UI Interface, Acronis True Image.

Está utilizando un ratón o tablilla USB Wacom. Hay una incompatibilidad entre los controladores Wacom y las herramientas gráficas que usa calibre. Pruebe con un ratón que no sea de la marca Wacom.

En algunas versiones de 64 bits de Windows existen configuraciones y software de seguridad que evitan que la versión de calibre 64 bits funcione correctamente. Si está utilizando la versión de calibre de 64 bits, intente cambiarla por la de 32 bits.

Si el error ocurre cuando intenta copiar texto del visor de libros de calibre, probablemente sea debido a alguna aplicación de gestión del portapapeles. Pruebe a desactivarla.

Si los fallos ocurren específicamente cuando usa un cuadro de diálogo de archivos, como al pulsar el botón Añadir libros o Guardar a disco, entonces tiene algún programa que ha instalado extensiones de consola defectuosas. Algunas posibilidades conocidas son: SpiderOak, odrive sync, Dell Backup and Recovery y NetDrive. Si tiene alguno de éstos, desinstálelo y todo irá bien. También puede usar la utilidad NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html] para ver qué extensiones de consola hay instaladas en el sistema y desactivarlas individualmente si no desea desinstalar el programa completo. Recuerde usar la opción «Restart Explorer» o reiniciar el equipo después de desactivar las extensiones de consola.

Si nada de lo anterior es aplicable a su caso, entonces hay algún otro programa en el equipo que está interfiriendo con calibre. En primer lugar, reinicie el equipo en modo seguro, para tener el menor número posible de programas en ejecución y ver si los problemas siguen sucediendo. Si no lo hacen, entonces sabrá que es un programa el que causa el problema. El culpable más probable es algún programa que modifique el comportamiento de otros programas, como un antivirus, un controlador de dispositivos, algo así RoboForm (una aplicación automática de rellenado de formularios) o una tecnología de asistencia como control de voz o un lector de pantalla.

La única manera de encontrar el culpable es eliminar los programas uno por uno y ver cuál está causando el problema. Básicamente, detenga un programa, ejecute calibre, verifique si hay problemas. Si se siguen sucediendo, detenga otro programa y repita.

Ni el visor ni la herramienta de modificación de libros electrónicos de calibre funcionan e Windows

Estos dos programas usan aceleración por hardware, pues incorporan una versión del navegador Chrome para mostrar el HTML. Si no funcionan, será debido a alguna incompatibilidad con los controladores gráficos (GPU) del sistema. Pruebe a actualizarlos y reiniciar el equipo. Si no se soluciona, puede establecer la variable de entorno QTWEBENGINE_CHROMIUM_FLAGS al valor --disable-gpu para desactivar la aceleración por hardware. Vea esta página [https://doc.qt.io/qt-6/qtwebengine-debugging.html] (en inglés) para más detalles.

Al usar el visor o realizar una conversión aparece un error de permiso denegado en Windows.

Algo en el equipo está impidiendo que calibre acceda a sus propios archivos temporales. Lo más probable es que los permisos de la carpeta Temp sean incorrectos. Vaya a la carpeta C:\Users\NOMBREDEUSUARIO\AppData\Local en el Explorador de Windows pulse con el botón derecho sobre la carpeta Temp, seleccione Propiedades y vaya a la pestaña Seguridad. Asegúrese de que su cuenta de usuario tiene control total para esta carpeta.

Algunos usuarios han informado de que la ejecución en una terminal de Administrador de la orden siguiente arregla sus permisos. Para acceder a una terminal de Administrador busque cmd.exe en el menú de inicio, pulse con el botón derecho en la entrada de símbolo del sistema y seleccione Ejecutar como administrador. En el símbolo del sistema, escriba la siguiente orden y pulse Intro:

icacls "%appdata%\..\Local\Temp" /reset /T

Otra opción es ejecutar calibre en modo Administrador, pero esto hará que algunas funciones, como arrastrar y soltar, no funcionen.

Por último, algunos usuarios han informado de que desactivar UAC soluciona el problema.

calibre no se inicia o se detiene en macOS

Una causa frecuente de fallos en macOS es el uso de tecnologías de accesibilidad no compatibles con las herramientas gráficas que usa calibre. Pruebe a desactivar VoiceOver si lo tiene activado. También vaya a Preferencias del Sistema > Acceso Universal y desactive la opción para permitir el acceso a dispositivos de asistencia en todas las pestañas. Otra causa puede ser el uso de herramientas de terceros que modifican el comportamiento del sistema, como Smart Scroll.

Puede obtener una salida de depuración sobre la causa de calibre no se inicie ejecutando Console.app. La salida de depuración se escribirá ahí. Si la salida de depuración contiene algo similar a:

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

entonces el problema es probablemente un caché de tipos de letra dañado. Puede borrar el caché siguiendo estas instrucciones [https://www.macworld.com/article/1139383/fontcacheclear.html] (en inglés). Si eso no lo resuelve, busque un archivo de tipo de letra dañado en su sistema, en file:~/Library/Fonts o similar. Una forma sencilla de buscar tipos de letra dañados en macOS es iniciar la aplicación «Font Book», seleccione todos los tipos de letra y luego en el menú Archivo, elija :guilabel`Validar tipos de letra`.

Sólo veo una pantalla negra o blanca al ejecutar el visor de libros electrónicos de calibre

Esto debe ser una incompatibilidad entre Qt WebEngine, que es lo que usa el visor para mostrar el libro, y los controladores de la GPU del sistema. Pruebe primero a actualizar los controladores de la GPU. Si eso no ayuda, puede probar a desactivar la aceleración de hardware de Qt WebEngine estableciendo la variable de entorno QTWEBENGINE_CHROMIUM_FLAGS al valor --disable-gpu. Para saber cómo cambiar las variables de entorno, vea Variables de entorno.

He descargado el instalador, pero no funciona.

Las descargas de Internet pueden resultar corruptas en ocasiones. Si el instalador de calibre que ha descargado no se abre, pruebe a descargarlo de nuevo. Si esto no funciona, descárguelo de una ubicación alternativa [https://github.com/kovidgoyal/calibre/releases/latest]. Si aún no funciona, algo en el equipo está evitando que se ejecute.

Pruebe deshabilitar temporalmente el programa antivirus (Microsoft Security Essentials, Kaspersky, Norton, McAfee o el que sea). Éste es probablemente el culpable de que el proceso de actualización se cuelgue a la mitad.

Igualmente, si el instalador falla o deshace los cambios y está ejecutando Microsoft PowerToys, ciérrelo.

Intente reiniciar el equipo y ejecutar un limpiador del registro como Wise registry cleaner [https://www.wisecleaner.com].

Pruebe una instalación limpia. Esto es, desinstale calibre, elimine C:\Program Files\Calibre2 (o donde previamente haya decidido instalar calibre). Después reinstale calibre. Tenga en cuenta que la desinstalación no afecta a sus libros o configuraciones.

Pruebe a descargar el instalador desde otro navegador. Por ejemplo, si está usando Microsoft Edge, pruebe con Firefox o Chrome en su lugar.

Si obtiene un error sobre un archivo DLL que falta en Windows, lo más probable es que los permisos de la carpeta temporal sean incorrectos. Vaya a la carpeta C:\Users\USERNAME\AppData\Local en el explorador de Windows, pulse con el botón derecho sobre la carpeta Temp y seleccione Propiedades y vaya a la pestaña Seguridad. Asegúrese de que su cuenta de usuario tiene control total para esta carpeta.

Si aun así no consique que funcione el instalador y está en Windows, puede usar la instalación portable de calibre [https://calibre-ebook.com/es/download_portable], que no necesita instalación (es sólo un archivo ZIP).

Mi programa antivirus dice que calibre es un virus o un troyano

Lo primero que debe comprobar es que está descargando calibre desde la web oficial [https://calibre-ebook.com/es/download]. Asegúrese de que pulsa en los enlaces de descarga a la izquierda, no en los anuncios de la derecha. calibre es un programa muy popular y la gente sin escrúpulos crea sitios web ofrefiendo su descarga para engañar a los incautos.

Si tiene la descarga oficial y su programa antivirus todavía dice que calibre es un virus, entonces es el programa antivirus el que está equivocado. Los programas antivirus utilizan heurística para detectar virus, patrones de código que «parecen sospechosos». Es algo así como la discriminación racial. calibre es un producto de código completamente abierto. En realidad puede ver el código fuente (o contratar a alguien para que lo haga por usted) para verificar que no es un virus. Por favor, informe de la identificación incorrecta a la compañía de la que obtenga el programa antivirus. Si el programa antivirus es el que impide la descarga o instalación de calibre, desactívelo temporalmente, instale calibre y luego vuelva a activarlo.

¿Cómo hago copias de respaldo de calibre?

Lo más importante para la copia de seguridad es la carpeta de la biblioteca de calibre, que contiene todos los libros y los metadatos. Ésta es la carpeta que eligió para la biblioteca de calibre cuando ejecutó calibre por primera vez. Puede obtener la ruta a la carpeta de la biblioteca pulsando sobre el icono de calibre en la barra de herramientas principal. Debe crear una copia de seguridad de esta carpeta con todos sus archivos y subcarpetas.

Puede hacer que calibre cambie a una copia de la carpeta de la biblioteca sin más que pulsar sobre el icono de calibre en la barra de herramientas y elegir la copia de la carpeta de biblioteca. Una copia de seguridad de la biblioteca contiene todas las columnas personalizadas y búsquedas guardadas, así como todos los libros y los metadatos.

Si desea hacer copias de seguridad de los complementos y de la configuración de calibre, tiene que realizar una copia de seguridad de la carpeta de configuración. Puede encontrar esta carpeta en Preferencias > Miscelánea. Tenga en cuenta que la restauración de carpetas de configuración no tiene soporte oficial, pero debería funcionar en la mayoría de los casos. Para restaurar sólo tiene que copiar el contenido de la carpeta de copia de seguridad en la carpeta de configuración actual.

¿Cómo uso en calibre los libros EPUB comprados (o qué hago con los archivos .acsm)?

La mayoría de los libros en formato EPUB que se compran tienen DRM. Esto hace que calibre no pueda abrirlos. Puede seguir utilizando calibre para almacenarlos y transferirlos a un lector de libros electrónicos. En primer lugar, debe autorizar el lector en una máquina Windows con Adobe Digital Editions. Una vez hecho esto, los libros en formato EPUB transferidos con calibre no tendrán ningún problema en el lector. Cuando compra un libro epub desde un sitio web, recibirá un archivo «.acsm». Este archivo debe abrirse con Adobe Digital Editions, que descargará el libro en formato «.epub». El archivo de libro electrónico se guardará en la carpeta My Digital Editions, desde donde se puede añadir a calibre.

Me sale un error de «Permiso denegado»

Un error de denegación de permiso puede ocurrir por muchas razones, ninguna de ellas tiene nada que ver con calibre.

Puede recibir errores de denegación de permiso si usa una tarjeta SD con la protección de escritura activada.

En macOS, si recibe un error de permiso al conectar un dispositivo a calibre, puede solucionarlo en Preferencias del sistema > Seguridad y privacidad > Privacidad > Archivos y carpetas.

Si usted, o algún programa que haya usado, cambia los permisos de los archivos en cuestión a sólo lectura.

Si hay un error de sistema de archivos en el dispositivo que hace que el sistema operativo monte el sistema de archivos en modo de sólo lecutura o marque un archivo concreto como de sólo lectura con recuperación necesaria.

Si los archivos tienen un propietario distinto a su usuario.

Si el archivo está abierto en otro programa.

Si el archivo está ubicado en un dispositivo, puede que haya alcanzado el límite máximo de 256 archivos en el directorio raíz del dispositivo. En tal caso deberá formatear el dispositivo o tarjeta SD indicado por el mensaje de error con un sistema de archivos FAT32, o borrar algunos archivos archivos de la tarjeta SD o la memoria del dispositivo.

Tendrá que corregir la causa subyacente del error de permisos antes de volver a utilizar calibre. Lea el mensaje de error con cuidado para ver a qué archivo se refiere y arregle los permisos en ese archivo o las carpetas que lo contienen.

¿Puedo hacer que los metadatos de comentario se muestren en el lector?

La mayoría de los lectores no admiten esto. Debería presentar una queja ante el fabricante al respecto, y quizá si suficientes personas se quejan las cosas cambien en el futuro. Mientras tanto, puede insertar los metadatos, incluyendo comentarios, en una «sobrecubierta» al comienzo del libro, mediante el uso de la opción Insertar metadatos en una página al principio del libro durante la conversión. La opción se encuentra en la sección Detección de estructura de las opciones de conversión. Tenga en cuenta que para que esto tenga efecto debe convertir el libro. Si el libro ya está en un formato que no necesita conversión, puede convertir de ese formato al mismo formato.

Otra posibilidad es crear un catálogo en forma de libro electrónico que contenga un listado de todos los libro en la biblioteca de calibre, con sus metadatos. Pulse y mantenga pulsado el botón Convertir acceder a la herramienta de creación de catálogos. Y antes que lo pregunte, no, no puede hacer que el catálogo «enlace directamente a» los libros en el lector.

¿Cómo hago que calibre utilice mi proxy HTTP?

De manera predeterminada, calibre utiliza la configuración de proxy establecida en el sistema operativo. A veces es incorrecta, por ejemplo, en Windows, si no utiliza Microsoft Edge la configuración de proxy puede no estar actualizada. Puede indicarle a calibre que use un servidor proxy en particular configurando las variables de entorno http_proxy y https_proxy. El formato de la variable es: http://usuario:contraseña@servidor. Debe pedirle a su administrador de red el valor correcto para esta variable. Tenga en cuenta que calibre sólo admite servidores proxy HTTP, no servidores SOCKS. Puede ver los proxies actualmente utilizados por calibre en Preferencias > Miscelánea.

Quiero que se añada una función a calibre. ¿Qué puedo hacer?

Tiene dos opciones:

Crear un parche modificando el código de calibre y enviarmelo para su revisión e inclusión. Vea Desarrollo [https://calibre-ebook.com/es/get-involved].

Abrir un informe de fallo para solicitar la función [https://calibre-ebook.com/es/bugs]. Recuerde que aunque pueda pensar que la función que solicita es muy importante o esencial, los desarrolladores de calibre pueden no estar de acuerdo. Por suerte, calibre es código abierto, lo que significa que siempre tiene la posibilidad de implementar la función por sí mismo o pagar a alguien que lo haga. Además, calibre tiene una arquitectura de complementos completa, por lo que puede desarrollar la función como un complemento, vea Escribir sus propios complementos para extender la funcionalidad de calibre.

¿Por qué calibre no se actualiza automáticamente?

Por muchos motivos:

No hay necesidad de actualizarlo cada semana. Si está contento con cómo funciona calibre desactive la notificación de actualizaciones y disfrute. Vuelva a comprobar si quiere actualizarlo cada año más o menos. Hay un casilla para desactivar la notificación de actualizaciones en la propia notificación de actualización.

Las descargas actuales de calibre consumen alrededor de 150 TB de ancho de banda al mes [https://calibre-ebook.com/dynamic/downloads]. Activar las descargas automáticas incrementaría este valor y acabaría costando miles de dólares al mes, que alguien tendría que pagar.

Si creo un cuadro de diálogo que descargue la actualización y la ejecute, en lugar de ir a la página web como ahora, eso ahorraría al más empedernido actualizador de calibre como mucho cinco pulsaciones a la semana. Hay cosas mucho más importantes que hacer en el desarrollo de calibre.

Si realmente, de verdad odia descargar calibre todas las semanas, pero aún quiere estar a la última, le animo a ejecutar calibre a partir del código fuente, lo que hace que la actualización sea trivial. Las instrucciones están disponibles aquí.

Hay actualizadores automáticos de calibre no oficiales, creados por usuarios de calibre, en el foro de calibre [https://www.mobileread.com/forums/forumdisplay.php?f=238] (en inglés).

¿Qué licencia tiene calibre?

calibre está licenciado bajo la Licencia Pública General de GNU v3 (una licencia de código abierto). Esto significa que usted es libre de redistribuir calibre, siempre y cuando proporcione también el código fuente. Así que si quiere poner calibre en un CD con su producto, también debe poner el código fuente de calibre en el CD. El código fuente está disponible para su descarga [https://download.calibre-ebook.com]. Es usted libre de utilizar los resultados de las conversiones de calibre como quiera. No puede utilizar ni el código o ni las librerías de calibre en su software sin hacer que su software de código abierto. Para más detalles, véase The GNU GPL v3 [https://www.gnu.org/licenses/gpl.html] (en inglés).

¿Cómo ejecuto calibre desde una memoria USB?

Hay una versión portátil de calibre disponible aquí [https://calibre-ebook.com/es/download_portable].

¿Cómo ejecuto partes de calibre como la descarga de noticias o el servidor de contenido en mi propio servidor Linux?

En primer lugar, debe instalar calibre en el servidor Linux. Si el servidor usa una distribución moderna de Linux, no debería tener ningún problema para instalar calibre.

Nota

calibre necesita GLIBC >= 2.31 y libstdc++ >= 6.0.28. Si tiene un servidor más antiguo tendrá que compilarlas a partir del código fuente o usar calibre 3.48, que requiere GLIBC &gt;= 2.17 o 2.85.1, que requiere GLIBC >= 2.13 o calibre 1.48, que requiere sólo GLIBC >= 2.10. Además, aunque las utilidades de línea de órdenes de calibre no necesitan un servidor X en ejecución, algunas de ellas necesitan que las bibliotecas de servidor X estén instaladas en el sistema. Esto es debido a Qt (que se usa para diversas tareas de procesado de imágenes) y enlaces con estas bibliotecas. Si obtiene un «ImportError» con respecto a algún módulo Qt, probablemente sea porque le falte alguna biblioteca X. Los candidatos habituales son: libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0.

Puede ejecutar el servidor de calibre mediante la orden:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

Puede descargar noticias y convertirlas en un libro electrónico con la orden:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

Si quiere generar un archivo MOBI, use outputfile.mobi y --output-profile kindle.

Puede enviar las noticias descargadas por correo electrónico con la orden:

/opt/calibre/calibre-smtp

La orden exacta la dejo como ejercicio para el lector.

Por último, puede añadir noticias descargadas a la biblioteca de calibre con:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

Recuerde leer la sección Interfaz de línea de órdenes del Manual de Usuario de Calibre para aprender más sobre estas y otras órdenes.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos

Cursillos

Aquí puede encontrar cursillos para iniciarse en las funciones más avanzadas de calibre, como XPath y plantillas.

Añadir su sitio de noticias favorito

Gestionar subgrupos de libros, por ejemplo «género»

Cursillo de XPath

El lenguaje de plantillas de calibre

Todo acerca de cómo utilizar expresiones regulares en calibre

Escribir sus propios complementos para extender la funcionalidad de calibre

Escribir fórmulas matemáticas en libros electrónicos

Crear catálogos AZW3 • EPUB • MOBI

Bibliotecas virtuales

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Gestionar subgrupos de libros, por ejemplo «género»

Gestionar subgrupos de libros, por ejemplo «género»

Algunas personas prefieren organizar los libros de su biblioteca en subgrupos, de forma similar a las subcarpetas. El motivo más frecuente es crear jerarquías de géneros, pero hay muchas otras. Un usuario pide una forma de organizar los libros de texto por número de curso y tema. Otro usuario quiere hacer un seguimiento de los regalos por tema y destinatario. Este cursillo usará el caso de los géneros para el resto de esta entrada.

Antes de continuar, tenga en cuenta que no estamos hablando de carpetas en el disco duro. Los subgrupos no son carpetas de archivos. Los libros no se copiarán en ningún lugar. La estructura de archivos de la biblioteca de calibre no se ve afectada. En su lugar, se trata de una forma de organizar y mostrar subgrupos de libros en una biblioteca calibre.

Configuración

Búsqueda

Restricciones

Funciones de plantilla útiles

Los requisitos normalmente establecidos para subgrupos tales como géneros son:

Un subgrupo (por ejemplo, un género) debe contener (o apuntar a) libros, no categorías de libros. Esto es lo que distingue los subgrupos de las categorías de usuario de calibre.

Un libro puede estar en varios subgrupos (géneros). Esto distingue a los subgrupos de las carpetas físicas de archivos.

Los subgrupos (géneros) deben formar una jerarquía; los subgrupos pueden contener subgrupos.

Las etiquetas satisfacen los dos primeros. Si etiqueta un libro con el género, puede utilizar el explorador de etiquetas (o una búsqueda) para encontrar los libros con ese género, lo que satisface el primer requisito. Varios libros pueden tener la misma etiqueta, lo que satisface el segundo. El problema es que las etiquetas no satisfacen el tercer requisito. No proporcionan una jerarquía.

árbol de grupos La función jerárquica de calibre proporciona la tercera opción, la posibilidad de ver los géneros como un «árbol» y la capacidad de buscar libros por género o subgénero. Por ejemplo, supongamos que la estructura de géneros es parecida a la siguiente:

Genre
    . History
    .. Japanese
    .. Military
    .. Roman
    . Mysteries
    .. English
    .. Vampire
    . Science Fiction
    .. Alternate History
    .. Military
    .. Space Opera
    . Thrillers
    .. Crime
    .. Horror
    etc.

Mediante la función de jerarquía se pueden ver estos géneros en el explorador de etiquetas en forma de árbol, como se muestra en la imagen de la pantalla. En este ejemplo, el nivel superior (Género) es una columna personalizada que contiene los géneros. Éstos contienen subgéneros que aparecen con un pequeño triángulo a su lado. Al pulsar en ese triángulo se abrirá el elemento y se mostrarán los subgéneros, como se puede ver con Historia y Ciencia ficción.

Al pulsar en un género puede buscar todos los libros con ese género sus descendientes. Por ejemplo, al pulsar en «Ciencia ficción» obtendrá también todos los libros en los géneros desdendientes: «Historia alternativa», «militar» y «Ópera espacial».. Al pulsar en «Historia alternativa» obtendrá los libros de ese género, y no los de los géneros «Militar» y «Ópera espacial». Por supuesto, un libro puede tener múltiples géneros. Si un libro tiene tanto los géneros «Ópera espacial» como «Militar», dicho libro aparecerá al pulsar en cualquiera de los dos géneros. Las búsquedas se discuten con más detalle a continuación.

Otra cosa que se puede ver en la imagen es que el género «Militar» aparece dos veces, una vez bajo «Historia» y otra bajo «Ciencia ficción». Dado que los géneros forman una jerarquía, se trata de dos géneros distintos. Un libro puede estar en uno, otro, o (poco probable en este caso) en ambos. Por ejemplo, los libros de la serio «La Segunda Guerra Mundial» de Winston Churchill podrían estar en «Historia.Militar». Los libros de la serie «Honor Harrington» de David Weber podrían estar en «Ciencia ficción.Militar», y ya puestos también en «Ciencia ficción.Ópera espacial».

Una vez que existe un género, es decir, que existe al menos un libro con ese género, puede aplicarlo fácilmente a otros libros arrastrando los libros desde la vista de biblioteca al género que desea que tengan los libros. También puede aplicar los géneros en los editores de metadatos; más sobre esto, más adelante.

Configuración

Por el momento puede que se esté preguntando cómo se ha conseguido todo esto. Hay tres pasos: 1) crear la columna personalizada, 2) comunicar a calibre que la nueva columna debe tratarse como una jerarquía y 3) añadir los géneros.

Cree la columna personalizada de la forma habitual, usando Preferencias > Interfaz > Añadir columnas personalizadas. Este ejemplo utiliza «#género» como el nombre de consulta y «Género» como el encabezado de columna. El tipo de columna es «texto separado por comas, como las etiquetas, se mostrará en el explorador de etiquetas.»

Después de reiniciar calibre, debe informar a calibre de que la columna debe ser tratada como una jerarquía. En Preferencias > Apariencia > Explorador de etiquetas escriba el nombre de consulta «#género» en «Categorías con elementos jerárquicos». Pulse Aplicar, y ya está concluida la configuración.

En este punto aún no hay géneros en la columna. Nos queda el último paso: cómo aplicar un género a un libro. Un género no existe en calibre hasta que aparece en al menos un libro. Para aprender cómo aplicar un género por primera vez, hay que entrar en algunos detalles acerca de cómo se muestra un género en los metadatos de un libro.

Una jerarquía de «cosas» se construye mediante la creación de un elemento que consiste en frases separadas por puntos. Continuando con el ejemplo de los géneros, estos elementos serían «Historia.Militar», «Misterio.Vampiros», «Ciencia ficción.Ópera espacial», etc. Por lo tanto, para crear un nuevo género, simplemente elija un libro que deba tener ese género, modifique sus metadatos e introduzca el nuevo género dentro de la columna que ha creado. Continuando con nuestro ejemplo, si desea asignar un nuevo género «Historietas» con un subgénero «Superhéroes» a un libro, activaría «Modificar metadatos» para ese libro, eligiría la pestaña de metadatos personalizados y a continuación introduciría «Historieta.Superhéroes» como se muestra a continuación (no preste atención a las otras columnas personalizadas):

Después de hacer lo anterior, el explorador de etiquetas mostrará:

A partir de ahora, para aplicar este nuevo género a un libro, puede arrastrar el libro sobre el género o añadirlo al libro usando el editor de metadatos, de la misma manera que se ha hecho anteriormente.

Nota

La vista jerárquica sólo funciona si el explorador de etiquetas está configurado para ordenar los elementos por nombre. Esto es así de manera predeterminada, y puede comprobarse pulsando en el botón Configurar en la parte inferior del explorador de etiquetas.

Búsqueda

La forma más fácil de buscar géneros es utilizar el explorador de etiquetas, pulsando en el género que desee ver. Al hacerlo sobre un género con descendientes se mostrarán los libros con ese género y todos los géneros de sus descendientes. Sin embargo, esto suscita una cuestión. El hecho de que un género tenga descendientes, no significa que no sea un género en sí mismo. Por ejemplo, un libro puede tener el género «Historia», pero no «Historia.Militar». ¿Cómo buscar libros con sólo «Historia»?

El mecanismo de búsqueda del explorador de etiquetas sabe si un elemento tiene descendientes. Si es así, al pulsar sobre el elemento se pasa sucesivamente por cinco búsquedas en lugar de las tres normales. La primera es el signo «+» verde, que muestra libros con sólo ese género (por ejemplo, «Historia»). El segundo es un doble signo «+» (mostrado arriba), que muestra libros con ese género y todos los subgéneros (por ejemplo, «Historia» e «Historia.Militar»). El tercero es el signo «-» rojo normal, que muestra los libros sin ese género exacto. El cuarto es un signo «-» duplicado, que muestra libros sin ese género o subgéneros. El quinto vuelve nuevamente al principio, ninguna marca, es decir, sin ninguna búsqueda.

Restricciones

Si busca un género, cree una búsqueda guardada; puede utilizar la opción «restringir a» para crear una biblioteca virtual de libros con ese género. Esto es útil si quiere hacer otras búsquedas dentro del género o para gestionar o actualizar metadatos de los libros de ese género. Continuando con nuestro ejemplo, puede crear una búsqueda guardada llamada «Historia.Japonesa» pulsando primero en el género «Japonesa» del explorador de etiquetas para obtener una búsqueda en el campo de búsqueda, introduciendo «Historia.Japonesa» dentro del campo de búsqueda guardada y a continuación pulsando el botón «guardar búsqueda» (el cuadro verde con el signo + blanco, en el lado derecho).

Una vez creada la búsqueda guardada, puede utilizarla como una restricción.

Funciones de plantilla útiles

Es posible que desee utilizar la información de género en una plantilla, tales como guardar en disco o enviar a dispositivo. La pregunta podría ser entonces: «¿Cómo puedo obtener ell nombre o nombres de género más externo?». Hay una función de plantilla de calibre, subítems, que hace esto más fácil.

Por ejemplo, supongamos que desea agregar el nivel de género más extrerno a la plantilla de guardar en disco para hacer carpetas de género, como «Historia/Se cierne la tormenta - Churchill, Winston». Para conseguirlo, debe extraer el primer nivel de la jerarquía y añadirlo al principio, junto con una barra para indicar que se debe hacer una carpeta. La siguiente plantilla hace esto:

{#genre:subitems(0,1)||/}{title} - {authors}

Vea El lenguaje de plantillas para más información sobre las plantillas y la función subitems().

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Cursillo de XPath

Cursillo de XPath

En este cursillo, se presentará una breve introducción a XPath [https://es.wikipedia.org/wiki/XPath], un lenguaje de consulta que se puede utilizar para seleccionar partes arbitrarias de documentos HTML [https://es.wikipedia.org/wiki/HTML] en calibre. XPath es un estándar ampliamente utilizado, y una búsqueda a través de Google proporcionará gran cantidad de información. Este cursillo, sin embargo, se centra en el uso de XPath para tareas relacionadas con libros electrónicos, como la búsqueda de títulos de capítulo en un documento HTML sin estructura.

Índice

Seleccionar por nombre de etiqueta

Seleccionar por atributos

Seleccionar por contenido de etiqueta

Libro de muestra

Funciones XPath predefinidas

Seleccionar por nombre de etiqueta

La forma más simple de selección es seleccionar las etiquetas por su nombre. Por ejemplo, suponga que desea seleccionar todas las etiquetas <h2> en un documento. La consulta XPath para esto es simplemente:

//h:h2        (Selects all <h2> tags)

El prefijo // significa buscar en cualquier nivel del documento. Supongamos que desea encontarar las etiquetas <span> que están dentro de etiquetas <a>. Esto se puede lograr con:

//h:a/h:span    (Selects <span> tags inside <a> tags)

Si quiere buscar etiquetas en un nivel particular del documento, cambie el prefijo:

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
             children of the <body> tag)

Esto coincidirá sólo con <p>A very short ebook to demonstrate the use of XPath.</p> en Libro de muestra, pero no con ninguna de las otras etiquetas <p>. El prefijo h: en los ejemplos anteriores se necesita para encontrar las etiquetas XHTML. Esto se debe a que internamente calibre representa todo el contenido como XHTML. En XHTML las etiquetas tienen un espacio de nombres, y h: es el prefijo de espacio de nombrs para las etiquetas HTML.

Ahora supongamos que desea seleccionar ambas etiquetas``<h1>`` y <h2>. Para hacer esto, necesitamos una construcción XPath llamada predicado. Un predicado es simplemente una comprobación que se utiliza para seleccionar etiquetas. Las comprobaciones pueden ser muy potentes y, según avance este cursillo, verá ejemplos más sofisticados. Un predicado se crea encerrando la expresión de comprobación entre corchetes:

//*[name()='h1' or name()='h2']

Hay varias características nuevas en esta expresión XPath. La primera es el uso del comodín *. Significa cualquier etiqueta. Ahora observe la expresión de comprobación name()='h1' or name()='h2'. name() es un ejemplo de función predefinida. Simplemente evalúa el nombre de la etiqueta. Por lo tanto, mediante su uso, podemos seleccionar etiquetas cuyo nombre sea h1 o h2. Tenga en cuenta que la función name() ignora los espacios de nombres de modo que no hay necesidad del prefijo h:. XPath tiene varias funciones predefinidas. Se presentarán algunas más en este cursillo.

Seleccionar por atributos

Para seleccionar etiquetas según sus atributos, es necesario usar predicados:

//*[@style]              (Select all tags that have a style attribute)
//*[@class="chapter"]    (Select all tags that have class="chapter")
//h:h1[@class="bookTitle"] (Select all h1 tags that have class="bookTitle")

Aquí, el operador @ se refiere a los atributos de la etiqueta. Puede utilizar algunas de las XPath built-in functions para realizar búsquedas más sofisticadas en los valores de los atributos.

Seleccionar por contenido de etiqueta

Utilizando XPath, puede incluso seleccionar etiquetas basadas en el texto que contienen. La mejor manera para para hacer esto es usar el poder de las expresiones regulares a través la función predefinida re:test():

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
                                          section)

Aquí el operador . se refiere a los contenidos de la etiqueta, igual que el operador @ se refiere a sus atributos.

Libro de muestra

<html>
    <head>
        <title>A very short e-book</title>
        <meta name="charset" value="utf-8" />
    </head>
    <body>
        <h1 class="bookTitle">A very short e-book</h1>
        <p style="text-align:right">Written by Kovid Goyal</p>
        <div class="introduction">
            <p>A very short e-book to demonstrate the use of XPath.</p>
        </div>

        <h2 class="chapter">Chapter One</h2>
        <p>This is a truly fascinating chapter.</p>

        <h2 class="chapter">Chapter Two</h2>
        <p>A worthy continuation of a fine tradition.</p>
    </body>
</html>

Funciones XPath predefinidas

name()

El nombre de la etiqueta actual.

contains()

contains(s1, s2) devuelve true si s1 contiene s2.

re:test()

re:test(fuente, patrón, opciones) devuelve true si el texto fuente coincide con la expresión regular patrón. Una opción particularmente útil es i, que hace que no se distinga entre mayúsculas y minúsculas. Una buena introducción a la sintaxis de las expresiones regulares se puede encontrar en sintaxis de expresiones regulares [https://docs.python.org/es/3/library/re.html]

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

El lenguaje de plantillas de calibre

El lenguaje de plantillas de calibre

El lenguaje de plantillas de calibre es un lenguaje específico de calibre que se utiliza en todo calibre para tareas como especificar rutas de archivo, dar formato a valores y calcular el valor de las columnas especificadas por el usuario. Ejemplos:

Especifar la estructura de carpetas y los nombres de archivo al guardar archivos de la biblioteca de calibre en el disco o en el lector de libros electrónicos.

Definir reglas para añadir iconos y colores a la lista de libros de calibre.

Definir columnas virtuales que contienen datos de otras columnas.

Búsqueda avanzada en la bibliotecas.

Búsqueda y sustitución avanzada de metadatos.

El lenguaje está construido en torno al concepto de una «plantilla», que especifica qué metadatos del libro se usan, las operaciones que se realizan sobre los metadatos y qué formato se aplica.

Plantillas básicas

Una plantilla básica consiste en una o varias expresiones de plantilla. Una expresión de plantilla consiste en texto y nombres entre llaves ({}) que se sustituyen por los metadatos correspondientes del libro que está siendo procesado. Por ejemplo, la plantilla predeterminada de calibre para guardar libros en un dispositivo tiene 4 expresiones de plantilla:

{author_sort}/{title}/{title} - {authors}

Para el libro «The Foundation» de «Isaac Asimov» da lugar a:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

Las barras no son expresiones de plantilla porque no están entre {}. Este tipo de texto se mantiene donde aparece. Por ejemplo, si la plantilla es:

{author_sort} Some Important Text {title}/{title} - {authors}

entonces para «The Foundation» la plantilla produce:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

Una expresión de plantilla tiene acceso a todos los metadatos disponibles en calibre, incluidas las columnas personalizadas que haya creado, usando su el nombre de búsqueda de una columna. Para obtener el nombre de búsqueda o consulta de una columna (también llamada campo), pase el cursor sobre el encabezado de la columna en la lista de libros de calibre. Los nombres de búsqueda para las columnas personalizadas siempre empiezan por #. Para columnas personalizadas de tipo serie, siempre hay un campo adicional llamado #nombre de búsqueda_index que es el índice de serie para el libro en la serie. Por ejemplo, si tiene una columna personalizada de serie llamada #miserie, también habrá una columna llamada #miserie_index. La columna de índice para la serie normal se llama series_index.

Además de los campos basados en columnas normales, también puede usar:

{formats} - Una lista de los formatos disponibles en la biblioteca de calibre para un libro

{identifiers:select(isbn)} - El ISBN del libro

Si un campo de metadatos para un libro determinado no está definido, entonces el campo en la plantilla se sustituye por un texto vacío (''). Por ejemplo, considere la siguiente plantilla:

{author_sort}/{series}/{title} {series_index}

Si el libro de Asimov «Second Foundation» está en la serie «Foundation», la plantilla produce:

Asimov, Isaac/Foundation/Second Foundation 3

Si no se ha introducido la serie para el libro, la plantilla produce:

Asimov, Isaac/Second Foundation

El procesador de plantillas elimina automáticamente barras múltiples y espacios iniciales o finales.

Formato avanzado

Además de sustituir metadatos, las plantillas pueden incluir texto adicional de manera condicional y controlar el formato de los datos sustituidos.

Incluir texto de mane condicional

A veces puede querer que aparezca un texto en la salida sólo si un campo no está vacío. Un caso común es series y series_index, para los que puede querer o nada o ambos valores separados por un guión. calibre tiene en cuenta este caso usando una sintaxis de expresión de plantilla especial.

Por ejemplo, y usando el caso anterior de la serie «Foundation», suponga que quiere que la plantilla produzca Foundation - 3 - Second Foundation. Esta plantilla cumple el cometido:

{series} - {series_index} - {title}

Sin embargo, si un libro no tiene serie, la plantilla producirá - - el título, que probablemente no sea lo que deseaba. Normalmente preferiría que el resultado fuese el título sin los guiones. Puede conseguir esto usando la siguiente sintaxis de plantilla:

{campo:|prefijo|sufijo}

Esta expresión de plantilla dice que si campo tiene el valor XXXX, el resultado será prefijoXXXXXsufijo. Si campo está vacío (no tiene ningún valor) el resultado será un texto vacío (nada), porque el prefijo y el sufijo se ignoran. Tanto el prefijo como el sufijo pueden contener espacios.

No use subplantillas ({…}) o funciones (ver más adelante) en el prefijo o el sufijo.

Usando esta sintaxis, podemos resolver el problema anterior de los libros sin serie con esta plantilla:

{series}{series_index:| - | - }{title}

Los guiones se incluirán solamente si el libro tiene índice de serie, que sólo tendrá si tiene una serie. Continuando con el ejemplo de la serie «Foundation», la plantilla producirá Foundation - 1 - Second Foundation.

Notas:

Debe incluir los dos puntos tras el nombre de búsqueda si usa un prefijo o sufijo.

Debe incluir ambos caracteres | o ninguno. No está permitido usar sólo uno como en {campo:| - }.

Es posible asignar un texto vació para el prefijo o sufijo, como en {series:|| - }. La plantilla {title:||} es lo mismo que {title}.

Formato

Supongamos que queremos asegurarnos de que series_index aparezca con dígitos, con ceros a la izquierda. Se consigue de esta manera:

{series_index:0>3s} - Tres dígitos con ceros a la izquierda

Para obtener ceros a la derecha use:

{series_index:0<3s} - Tres dígitos con ceros a la derecha

Si usa índices de serie con decimales (por ejemplo 1.1), puede querer asegurarse de que los decimales queden alineados. Por ejemplo, puede que los índices 1 y 2.5 aparezcan como 01.00 y 02.50 para que se ordenen correctamente en un dispositivo que utilice un orden lexicográfico. Para ello, use:

{series_index:0>5.2f} - Cinco caracteres que incluyen dos dígitos con cero a la izquierda, un punto decimal y dos dígitos tras el punto decimal.

Si quiere sólo las dos primeras letras de los datos, use:

{author_sort:.2} - Sólo las primeras dos letras del orden de autor

Gran parte del formato del lenguaje de plantillas de calibre proviene de Python. Para obtener más detalles sobre la sintaxis de estas operaciones de formato avanzadas, vea la documentación de Python [https://docs.python.org/3/library/string.html#formatstrings] (en inglés).

Usar plantillas para definir columnas personalizadas

Las plantillas pueden usarse para mostrar información que no está en los metadatos de calibre o para mostrar los metadatos de una manera diferente al formato normal de calibre. Por ejemplo, puede querer mostrar el ISBN, un campo que calibre no muestra. Puede conseguirlo creando una columna personalizadad de tipo columna generada a partir de otras columnas (en lo sucesivo llamadas columnas compuestas), e introduciendo una plantilla para generar el texto mostrado. La columna mostrará el resultado de evaluar la plantilla. Por ejemplo, para mostrar el ISBN, cree la columna e introduzca {identifiers:select(isbn)} en el cuadro de plantilla. Para mostrar una columna que contenga los valores de dos campos personalizados de serie separados por una coma, use {#serie1:||,}{#serie2}.

Las columnas compuestas pueden utilizar cualquier opción de plantilla, incluidas las de formato.

Nota: No puede modificar los datos mostrados en una columna compuesta, debe modificar las columnas de origen. Si modifica una columna compuesta, por ejemplo pulsando dos veces sobre ella, calibre abrirá la plantilla para modificarla, no los datos resultantes.

Plantillas y controles de metadatos

Los paneles de conexiones se usan para cambiar los metadatos escritos en los libros durante las operaciones de guardado en disco y de envío a dispositivo. Un panel de conexiones le permite especificar una plantilla para suministrar los datos que se escribirán en los metadatos del libro. Puede usar los paneles de conexiones para modificar los siguientes campos: authors, author_sort, language, publisher, tags, title, title_sort. Esa función es útil para los que quieren usar metadatos diferentes en los libros de los dispositivos, para solucionar problemas de ordenación o de visualización.

Cuando cree un panel de conexiones, especifique el formato y dispositivo para los que se usará. Hay un dispositivo especial save_to_disk, que se usa para guardar formatos (en lugar de enviarlos a un dispositivo). Una vez que ha elegido el formato y dispositivo, elija los campos de metadatos para cambiar, y suministre plantillas para obtener los nuevos valores. Estas plantillas están conectadas con sus campos de destino, de ahí el nombre panel de conexiones. Por supuesto, puede usar columnas compuestas en estas plantillas.

Los paneles de conexiones son muy flexibles y pueden escribirse en modo de función única, en modo programación de plantillas, en modo de programación general o en modo de plantilla de Python.

Cuando un panel de conexiones pueda aplicarse (servidor de contenido, guardado en disco o envío a dispositivo), calibre busca los paneles definidos para elegir el correcto según el formato y dispositivo. Por ejemplo, para encontrar el panel de conexiones apropiado para enviar un libro EPUB a un dispositivo ANDROID, calibre busca en los paneles en el siguiente orden:

un panel de conexiones con una coincidencia exacta de formato y dispositivo, por ejemplo: EPUB y ANDROID

un panel de conexiones con una coincidencia exacta de formato y el dispositivo especial any device, por ejemplo EPUB y any device

un panel de conexiones con el formato especial any format y una coincidencia exacta de dispositivo, por ejemplo: any format y ANDROID

un panel de conexiones con any format y any device

Los campos etiquetas y autores tienen un trato especial, debido a que ambos pueden tener más de un elemento. Un libro puede poseer varias etiquetas y varios autores. Cuando indique que desea cambiar uno de estos campos, la plantilla se examina para comprobar si hay más de un elemento. Para las etiquetas, el resultado se divide dondequiera que calibre encuentre una coma. Por ejemplo, si la plantilla produce el valor Intriga, Terror, el resultado serán dos etiquetas: Intriga y Terror. No existe manera de poner una coma dentro de una etiqueta.

Lo mismo ocurre con los autores, pero usando un carácter diferente para el corte, un signo «&» en lugar de una coma. Por ejemplo, si la plantilla produce el valor Blogs, Joe&Posts, Susan, el libro acabará con dos autores, Blogs, Joe y Posts, Susan. Si la plantilla produce el valor Blogs, Joe;Posts, Susan, el libro tendrá un autor con un nombre peculiar.

Los paneles de conexiones afectan a los metadatos escritos en el libro cuando se guarda en disco o se escribe en un dispositivo. Los paneles de conexiones no afectan a los metadatos usados por las funciones Guardar en el disco Enviar al dispositivo para crear los nombres de archivo. En lugar de ello, los nombres de archivo se construyen usando las plantillas introducidas en la ventana de preferencias correspondiente.

Usar funciones en plantillas: modo de función única

Supongamos que desea mostrar el valor de un campo en mayúsculas, aunque normalmente el valor del campo tiene sólo las iniciales en mayúscula. Puede conseguir esto usando las funciones de plantilla. Por ejemplo, para mostrar el título en mayúsculas use la función uppercase, como en {title:uppercase()}. Para mostrarlo con las iniciales en mayúscula use {title:titlecase()}.

Las funciones van en la parte del formato de la plantilla, después de : y antes del primer | o del } de cierre si no se usa ningún prefijo o sufijo. Si tiene tanto un formato como una referencia de función, la función va después de otro :. Las funciones devuelven el valor de la columna especificada en la plantilla, modificado de manera adecuada.

La sintaxis para usar funciones es una de:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

Los nombres de función deben is siempre seguidos de paréntesis de apertura y cierre. Algunas funciones requieren valores adicionales (argumentos), y éstos van dentro de los paréntesis. Los argumentos se separan por comas. Una coma literal (como texto, no como separador de argumentos) debe ir precedida de una barra invertida (\). El último (o único) argumento no puede contener un paréntesis de cierre textual.

Las funciones se evalúan antes de las especificaciones de formato y del prefijo y sufijo. Véase más abajo un ejemplo de cómo usar un formato y una función.

Importante: Si tiene experiencia en programación, tenga en cuenta que la sintaxis del modo de función única no es la que podría esperarse. Los textos van sin comillas y los espacios son importantes. Todos los argumentos se interpretan como constantes; no hay expresiones.

No use subplantillas ({…}) como argumentos de función. En su lugar, use el modo de programación de plantilla o el modo de programación general.

Algunas funciones requieren expresiones regulares. En el lenguaje de plantillas, las expresiones regulares no distinguen entre mayúsculas y minúsculas.

En la documentación de funciones que se presenta a continuación, la notación [algo]* significa que algo puede repetirse cero o más veces. La notación [algo]+ significa que algo se repite una o más veces (debe existir al menos una vez).

Las funciones pensadas para usarse en modo de función única son:

capitalize() – devuelve el valor con la primera letra mayúscula y el resto en minúsculas.

contains(patrón, texto_si_coincide, texto_si_no_coincide) – comprueba si el valor coincide con la expresión regular patrón. Devuelve texto_si_coincide si el patrón coincide con el valor, en caso contrario devuelve texto_si_no_coincide.

count(separador) – interpreta el valor como una lista de elementos separados por separador y devuelve el número de elementos de la lista. La mayoría de las listas usan una coma como separador, pero authors usa un «ampersand» (&). Ejemplos: {tags:count(,)}, {authors,count(&)}. Sinónimos: count(), list_count()

format_number(plantilla) – interpreta el valor como un número y le da formato usando una plantilla de formato de Python como {0:5.2f} o {0:,d} o ${0:5,.2f}. La plantilla de formato debe empezar por {0: y terminar por }, como en los ejemplos anteriores. Excepción: puede omitir el {0: inicial y el } final si la plantilla de formato contiene sólo un formato. Véase la documentación del lenguaje de plantillas y de Python [https://docs.python.org/3/library/string.html#formatstrings] para más ejemplos. Devuelve un texto vacío si el formato falla.

human_readable() – espera que el valor sea un número y devuelve un texto que representa ese número en KB, MB, GB, etc.

ifempty(texto_si_vacío) – si el valor no está vacío, devuelve el valor del campo, en caso contrario devuelve texto_si_vacío.

in_list(separador, [ patrón, encontrado, ]* no_encontrado) – interpreta el valor como una lista de elementos separados por separador y comprueba patrón para cada elemento de la lista. Si patrón coincide con algún elemento, devuelve encontrado, en caso contrario devuelve no_encontrado. La pareja de argumentos patrón y encontrado puede repetirse tantas veces como se desee, lo que permite devolver diferentes valores según el valor del elemento. Los patrones se evalúan en orden y se devuelve la primera coincidencia.

language_strings(localizar) – devuelve los nombres de los idiomas [https://www.loc.gov/standards/iso639-2/php/code_list.php] identificados por los códigos de idioma [https://www.loc.gov/standards/iso639-2/php/code_list.php] pasados como valor. Ejemplo {languages:language_strings()}. Si localizar es cero, devuelve los nombres en inglés. Si localizar no es cero, devuelve los nombres en el idioma actual. Los códigos de idioma son una lista separada por comas.

list_item(índice, separador) – interpreta el valor como una lista de elementos separados por separador y devuelve el elemento número índice. El primer elemento es el número cero. El último elemento tiene índice -1 y puede obtenerse con list_item(-1,separador). Si el elemento no está en la lista devuelve un texto vacío.

lookup([ patrón, clave, ]* clave_alternativa) – Los patrones se comparan con el valor en orden. Si un patrón coincide, se devuelve el valor del campo indicado por clave. Si no coincide ningún patrón, se devuelve el valor del campo indicado por clave_alternativa. Véase switch (más abajo).

lowercase() – devuelve el valor del campo en minúsculas.

rating_to_stars(usar_media_estrella) – Devuelve la calificación como caracteres de estrella (★). El valor debe ser un número entre 0 y 5. Ponga usar_media_estrella a 1 si quiere caracteres de media estrella para números fraccionarios, disponibles con columnas personalizadas de calificación.

re(patrón, sustitución) – devuelve el valor después de aplicar la expresión regular. Todas las veces que aparezca patrón en el valor se sustituirá por sustitución. El lenguaje de plantillas usa ``expresiones regulares de Python <https://docs.python.org/3/library/re.html>`_, sin distinguir mayúsculas y minúsculas.

select(clave) – interpreta el valor como una lista de elementos separados por comas, con cada elemento de la forma id:valor (el formato de la columna identifier de calibre). La función encuentra la primera pareja con id igual a clave y devuelve el valor correspondiente. Si no se encuentra el id la función devuelve un texto vacío.

shorten(car_izq, texto_medio, car_der) – devuelve una versión abreviada del valor, consistente en un número car_izq de caracteres del principio del valor, seguidos de texto_medio, seguido de un número car_der de caracteres del final del valor. car_izq y car_der deben ser números enteros no negativos. Ejemplo, supongamos que quiere mostrar el título con una longitud máxima de 15 caracteres. Una plantilla que hace esto es {title:shorten(9,-,5)}. Para un libro con el título Novísima recopilación de las leyes de España el resultado será Novísima -spaña: los primeros 9 caracteres del título, un - y los últimos 5 caracteres. Si la longitud del valor es menor que car_izq + car_der + la longitud de texto_medio, se usará el valor intacto. Por ejemplo, el título La colmena no se cambiará.

str_in_list(separador, [ texto, encontrado, ]+ no_encontrado) – interpreta el valor como una lista de elementos separados por separador, y compara texto con cada valor de la lista. El argumento texto no es una expresión regular. Si texto es igual a alguno de los valores (sin distinción de mayúsculas y minúsculas), devuelve el correspondiente encontrado. Si texto contiene separador, se interpreta también como una lista y se compara cada subvalor. La pareja de argumentos texto y encontrado puede repetirse tantas veces como se desee, lo que permite devolver diferentes valores según el valor de texto. Los textos se comparan en orden. Se devuelve la primera coincidencia.

subitems(índice_inicio, índice_fin) – Esta función separa listas de elementos jerárquicos de tipo etiqueta, tales como los géneros. Interpreta val como una lista de elementos separados por comas, donde cada elemento es a su vez una lista de elementos separados por puntos. Devuelve una nueva lista formada tomando de cada elemento los componentes entre las posiciones índice_inicio e índice_fin y combinando los resultados. Se eliminan los duplicados. El primer subelemento de cada lista separada por puntos ocupa la posición cero. Si un índice es negativo, se cuenta desde el final de la lista. Como caso especial, si índice_fin es cero, se considera que es el final de la lista.

Ejemplos:

Suponiendo que la columna #genero contiene A.B.C:

{#genero:subitems(0,1)} devuelve A

{genero:subitems(0,2)} devuelve A.B

{#genero:subitems(1,0)} devuelve B.C

Suponiendo que la columna #genero contiene A.B.C, D.E:

{#genero:subitems(0,1)} devuelve A, D

{#genero:subitems(0,2)} devuelve A.B, D.E

sublist(índice_inicio, índice_fin, separador) – interpreta el valor como una lista de elementos separados por separador y devuelve una nueva lista con los elementos comprendidos entre la posición índice_inicio e índice_fin. El primer elemento ocupa la posición cero. Si un índice es negativo, se cuenta desde el final de la lista. Como caso especial, si índice_fin es cero, se considera que es el final de la lista.

Ejemplos suponiendo que la columna tags (separada por comas) contiene A, B, C:

{tags:sublist(0,1,\,)} devuelve A

{tags:sublist(-1,0,\,)} devuelve C

{tags:sublist(0,-1,\,)} devuelve A, B

swap_around_articles(separador) – devuelve el valor con los artículos puestos al final. El valor puede ser una lista, en cuyo caso se procesará cada elemento de la lista. Si el valor es una lista, debe proporcionar el separador de los elementos de la lista. Si no se proporciona ningún separador, la lista se trata como un único valor, no una lista. Los artículos son los que calibre usa para generar el orden de título.

swap_around_comma() – dado un valor de la forma B, A, devuelve A B. Esto es útil para convertir nombres en formato APELLIDO, NOMBRE a NOMBRE APELLIDO. Si no hay ninguna coma en el valor, la función devuelve el valor sin cambios.

switch([patrón, valor,]+ otro_valor) – para cada pareja patrón, valor, comprueba si el valor contiene coincidencias para la expresión regular patrón y, en tal caso, devuelve el valor asociado. Si no coincide ningún patrón, devuelve otro_valor. Pueden emplearse tantas parejas patrón, valor como se desee. Se devuelve la primera coincidencia.

test(texto_si_no_vacío, texto_si_vacío) – devuelve texto_si_no_vacío si el valor no está vacío, devuelve texto_si_vacío en caso contrario.

titlecase() – devuelve el valor del campo con las iniciales en mayúscula.

transliterate() – Devuelve un texto en el alfabeto latino formado por aproximación del sonido de las palabras en el campo de origen. Por ejemplo, si el campo de origen es Фёдор Миха́йлович Достоевский la función devuelve Fiodor Mikhailovich Dostoievskii.

uppercase() – devuelve el valor del campo en mayúsculas.

Usar funciones y formato en la misma plantilla

Supongamos que tiene una columna personalizada con números enteros #myint, que quiere mostrar con ceros a la izquierda, como en 003. Una posibilidad es usa como formato 0>3s. Sin embargo, de manera predeterminada, si un número (entero o decimal) es igual a cero, el valor se muestra como un texto vacío, así que el valor cero producirá un texto vacío, no 000. Si quiere ver los valores como 000, debe usar tanto el texto de formato como la función ifempty para cambiar el valor vacío a cero de nuevo, la plantilla sería:

{#myint:0>3s:ifempty(0)}

Tenga en cuenta que puede usar también prefijo y sufijo. Si desea que el número aparezca como [003] o [000], use la plantilla:

{#myint:0>3s:ifempty(0)|[|]}

Modo de programación general

El Modo de programación general (MPG) sustituye las expresiones de plantilla por un programa escrito en el lenguaje de plantillas. La sintaxis del lenguaje está definida por la siguiente gramática:

program         ::= 'program:' expression_list
expression_list ::= top_expression [ ';' top_expression ]*
top_expression  ::= or_expression
or_expression   ::= and_expression [ '||' and_expression ]*
and_expression  ::= not_expression [ '&&' not_expression ]*
not_expression  ::= [ '!' not_expression ]* | concatenate_expr
concatenate_expr::= compare_expr [ '&' compare_expr ]*
compare_expr    ::= add_sub_expr [ compare_op add_sub_expr ]
compare_op      ::= '==' | '!=' | '>=' | '>' | '<=' | '<' | 'in' | 'inlist' |
                    '==#' | '!=#' | '>=#' | '>#' | '<=#' | '<#'
add_sub_expr    ::= times_div_expr [ add_sub_op times_div_expr ]*
add_sub_op      ::= '+' | '-'
times_div_expr  ::= unary_op_expr [ times_div_op unary_op_expr ]*
times_div_op    ::= '*' | '/'
unary_op_expr   ::= [ add_sub_op unary_op_expr ]* | expression
expression      ::= identifier | constant | function | assignment | field_reference |
                    if_expr | for_expr | break_expr | continue_expr |
                    '(' expression_list ')' | function_def
field_reference ::= '$' [ '$' ] [ '#' ] identifier
identifier      ::= id_start [ id_rest ]*
id_start        ::= letter | underscore
id_rest         ::= id_start | digit
constant        ::= " string " | ' string ' | number
function        ::= identifier '(' expression_list [ ',' expression_list ]* ')'
function_def    ::= 'def' identifier '(' top_expression [ ',' top_expression ]* ')' ':'
                    expression_list 'fed'
assignment      ::= identifier '=' top_expression
if_expr         ::= 'if' condition 'then' expression_list
                    [ elif_expr ] [ 'else' expression_list ] 'fi'
condition       ::= top_expression
elif_expr       ::= 'elif' condition 'then' expression_list elif_expr | ''
for_expr        ::= for_list | for_range
for_list        ::= 'for' identifier 'in' list_expr
                    [ 'separator' separator_expr ] ':' expression_list 'rof'
for_range       ::= 'for' identifier 'in' range_expr ':' expression_list 'rof'
range_expr      ::= 'range' '(' [ start_expr ',' ] stop_expr
                    [ ',' step_expr [ ',' limit_expr ] ] ')'
list_expr       ::= top_expression
break_expr      ::= 'break'
continue_expr   ::= 'continue'
separator_expr  ::= top_expression
start_expr      ::= top_expression
stop_expr       ::= top_expression
step_expr       ::= top_expression
limit_expr      ::= top_expression

Notas:

una top_expression siempre tiene un valor. El valor de una expression_list es el valor de la última top_expression de la lista. Por ejemplo, el valor de la expression_list 1;2;'blabla';3 es 3.

En un contexto lógico, cualquier valor que no sea vacío es True (verdadero)

En un contexto lógico, el valor vacío es False (falso)

Textos y números pueden usarse indistintamente. Por ejemplo, 10 y '10' son lo mismo.

Los comentarios son líneas que empiezan por un carácter #. No se permiten comentarios que empiecen a mitad de línea.

Prioridad de operadores

La prioridad de los operadores (orden de evaluación) de mayor (se evalúa primero) a menor (se evalúa el último) es:

Llamadas a función, constantes, expresiones entre paréntesis, expresiones de declaración, expresiones de asignación, referencias de campo.

Más (+) y menos (-) unarios. Estos operadores se evalúan de derecha a izquierda.

Estas y todas las otras operaciones aritméticas devuelven enteros si la expresión resulta en una parte decimal igual a cero. Por ejemplo, si la expresión devuelve 3.0 se cambia a 3.

Multiplicación (*) y división (/). Estas operaciones son asociativas y se evalúan de izquierda a derecha. Use paréntesis si quiere cambiar el orden de evaluación.

Suma (+) y resta (-). Estas operaciones son asociativas y se evalúan de izquierda a derecha.

Comparaciones numéricas y de textos. Estas operaciones devuelven '1' si la comparación tiene éxito, en caso contrario un texto vacío (''). Las comparaciones no son asociativas: a < b < c es un error de sintaxis.

Concatenación de textos (&). El operador & devuelve un texto formado por concatenación de las expresiones a su izquierda y derecha. Ejemplo: 'aaa' & 'bbb' devuelve 'aaabbb'. El operador es asociativo y se evalúa de izquierda a derecha.

Negación lógica unaria (!). Esta operación devuelve '1' si la expresión es False (da como resultado un texto vacío), en caso contrario ''.

Y lógico (&&). Esta operación devuelve '1' si ambas expresiones a derecha e izquierda son True, o un texto vacío ('') si alguna es False. Es asociativa, se evalúa de izquierda a derecha y con cortocircuito https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html.

O lógico (||). Esta operación devuelve '1' si alguna de las expresiones a derecha e izquierda es True, o un '' si ambas son False. Es asociativa, se evalúa de izquierda a derecha y con cortocircuito https://chortle.ccsu.edu/java5/Notes/chap40/ch40_8.html.

Referencias de campo

Una field_reference da como resultado el valor del campo de metadatos indicado por el nombre de búsqueda que sigue al $ o $$. Usar $ es equivalente a usar la función field. Usar $ es equivalente a usar la función raw_field. Ejemplos:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

Expresiones condicionales

Las expresiones condicionales (if_expression) evalúan primero la condition. Si la condition es True (un valor no vacío) entonces se evalúa expression_list en la sentencia then. Si es False, entonces se evalúa expression_list en la sentencia elif o else, si existen. Las partes elif y else son opcionales. Las palabras if, then, elif, else y fi están reservadas, no pueden usarse como nombres de identificador. Se pueden añadir espacios y saltos de línea donde sea conveniente. La condition es una top_expression, no usa expression_list, no se permite punto y coma. Los elementos de tipo expression_list son secuencias de top_expression separadas por punto y coma. Una expresión condicional devuelve el resultado de la última top_expression en la expression_list evaluada, o un texto vacío si no se evaluó ninguna expression_list.

Ejemplos:

* program: if field('series') then 'yes' else 'no' fi
* program:
      if field('series') then
          a = 'yes';
          b = 'no'
      else
          a = 'no';
          b = 'yes'
      fi;
      strcat(a, '-', b)

Ejemplo de if anidado:

program:
  if field('series') then
    if check_yes_no(field('#mybool'), '', '', '1') then
      'yes'
    else
      'no'
    fi
  else
    'no series'
  fi

Como se ha mencionado, un if produce un valor. Esto significa que todas las siguientes expresiones son equivalentes:

* program: if field('series') then 'foo' else 'bar' fi
* program: if field('series') then a = 'foo' else a = 'bar' fi; a
* program: a = if field('series') then 'foo' else 'bar' fi; a

Como último ejemplo, este programa devuelve el valor de la columna series si el libro tiene una serie, en caso contrario el valor de la columna title:

program: field(if field('series') then 'series' else 'title' fi)

Expresiones de bucle

Una expresión de bucle (for_expression) itera sobre una lista de valores, procesando uno cada vez. La list_expression debe resultar en el nombre de búsqueda de un campo de metadatos, como tags o #genero, o una lista de valores. La función range() (ver más adelante) genera una lista de números. Si el resultado es un nombre de búsqueda válido, se obtiene el valor del campo y se usa el separador especificado para ese tipo de campo. Si el resultado no es un nombre de búsqueda válido, se supone que es una lista de valores. La lista se supone separada por comas, a no ser que use la palabra clave opcional separator, en ese caso los valores de la lista deben estar separados por el resultado de evaluar separator_expr. No se puede usar un separador si la lista es generada por range(). Cada valor de la lista se asigna a la variable especificada y luego se evalúa expression_list. Puede usar break para salir del bucle y continue para comenzar con la siguiente iteración.

Ejemplo: Esta plantilla elimina el primer nombre jerárquico para cada valor de Género (#genre`), construyendo una lista con los nuevos nombres.

program:
  new_tags = '';
  for i in '#genre':
    j = re(i, '^.*?\.(.*)$', '\1');
    new_tags = list_union(new_tags, j, ',')
  rof;
  new_tags

Si el Género original es Historia.Militar, Ciencia ficción.Historia alternativa, Léeme, la plantilla devolverá Militar, Historia alternativa, Léeme. Puede usar esta plantilla en Modificar metadatos en masa > Buscar y sustituir con Buscar establecido en plantilla para eliminar el primer nivel de la jerarquía y asignar el valor resultante a Género.

Nota: la última línea en la plantilla, new_tags, no es estrictamente necesaria en este caso, porque for devuelve el valor de la última top_expression en expression_list. El valor de una asignación (assignment) es el valor de su expresión, así que el valor del for es lo que se asignó a new_tags.

Definición de funciones

Si tiene una parte del código en una plantilla que se repite, puede poner ese código en una función local. La palabra clave def comienza la definición. Le sigue el nombre de la función, la lista de argumentos y el código en la función. La definición de función termina con la palabra clave fed.

Los argumentos son posicionales. Cuando se invoca una función, los argumentos proporcionados se hacen corresponder de izquierda a derecha con los parámetros definidos, con el valor del argumento asignado al parámetro. Es un error proporcionar más argumentos que parámetros definidos. Los parámetros pueden tener valores predeterminados, como a = 25. Si no se proporciona un argumento para un parámetro, se usará el valor predeterminado, en caso contrario el parámetro será un texto vacío.

La instrucción return se puede usar en una función local.

Una función debe definirse antes de poder usarse.

Ejemplo: Esta plantilla calcula una duración aproximada en años, meses y días a partir de un número de días. La función to_plural() da formato a los valores calculados. Tenga en cuenta que el ejemplo también usa el operador &:

program:
      days = 2112;
      years = floor(days/360);
      months = floor(mod(days, 360)/30);
      days = days - ((years*360) + (months * 30));

      def to_plural(v, str):
              if v == 0 then return '' fi;
              return v & ' ' & (if v == 1 then str else str & 's' fi) & ' '
      fed;

      to_plural(years, 'year') & to_plural(months, 'month') & to_plural(days,'day')

Operadores de relación

Los operadores de relación devuelven '1' si la comparación es cierta, en caso contrario devuelven un texto vacío ('').

Hay dos formas de operadores de relación: comparaciones de texto y comparaciones numéricas.

Las comparaciones de texto usan el orden lexicográfico y no distinguen mayúsculas y minúsculas. Las comparaciones de texto permitidas son ==, !=, <, <=, >, >=, in e inlist. El operador in es True si el valor de la expresión de la izquierda, interpretado como una expresión regular, coincide con el valor de la expresión de la derecha. El operador inlist es True si el valor de la expresión de la izquierda, interpretado como una expresión regular coincide con alguno de los elementos de la lista de la derecha, en la que los elementos están separados por comas. Las comparaciones no distinguen mayúsculas y minúsculas.

Los operadores de comparación numérica son ==#, !=#, <#, <=#, ># y >=#. Las expresiones de la izquierda y la derecha deben resultar en valores numéricos con dos excepciones: Tanto el valor textual None (campo no definido) como el texto vacío equivalen al valor cero.

Ejemplos:

program: field('series') == 'lala' devuelve '1' si la serie del libro es lala, en caso contrario devuelve ''.

program: 'l.a' in field('series') devuelve '1' si la serie del libro coincide con la expresión regular l..a (por ejemplo, lala, El asesino de reyes, etc.), en caso contrario devuelve ''.

program: 'ciencia' inlist field('#genero') devuelve '1' si alguno de los géneros del libro coincide con la expresión regular ciencia (por ejemplo Ciencia, Historia de la ciencia, Ciencia ficción, etc.), en caso contrario devuelve ''.

program: '^ciencia$' inlist field('#genero') devuelve '1' si alguno de los géneros del libro coincide con la expresión regular ^ciencia$ (por ejemplo Ciencia). Los géneros Historia de la ciencia y Ciencia ficción no coinciden. Si no hay ninguna coincidencia, devuelve ''.

program: if field('series') != 'lala' then 'nana' else 'blabla' fi devuelve 'nana' si la serie del libro no es lala, en caso contrario devuelve 'blabla'.

program: if field('series') == 'lala' || field('series') == '1632' then 'sí' else 'no' fi devuelve 'sí' si la serie es lala o 1632, en caso contrario devuelve 'no'.

program: if '^(lala|1632)$' in field('series') then 'sí' else 'no' fi devuelve 'sí' si la serie es lala o 1632, en caso contrario devuelve 'no'.

program: if 11 > 2 then 'sí' else 'no' fi devuelve 'no' porque el operador > realiza una comparación lexicográfica.

program: if 11 ># 2 then 'sí' else 'no' fi devuelve 'sí' porque el operador ># realiza una comparación numérica.

Otras funciones disponibles

Las siguientes funciones están disponibles, además de las descritas en el modo de función única.

En el MPG todas las funciones descritas en el modo de función única requieren un primer argumento adicional que especifique el valor sobre el que operan. Todos los argumentos son de tipo expression_list (ver la gramática más arriba).

add(x [, y]*) – devuelve la suma de sus argumentos. Da un error si algún argumento no es un número. En la mayoría de los casos puede usarse el operador + en lugar de esta función.

and(valor [, valor]*) – devuelve '1' si todos los valores son no vacíos, en caso contrario devuelve un texto vacío. Pueden usarse tantos valores como se desee. En la mayoría de los caso se puede usar el operador && en lugar de esta función. Un motivo para no sustituir and por && es cuando la evaluación en cortocircuito puede cambiar el resultado debido a los efectos secundarios. Por ejemplo, and(a='',b=5) realizará las dos asignaciones, mientra que el operador && no realizará la segunda.

assign(id, val) – asigna val a id y devuelve val. id debe ser un identificador, no una expresión. En la mayoría de los casos puede usarse el operador = en lugar de esta función.

approximate_formats() – devuelve una lista separada por comas de formatos asociados con el libro. No hay garantía de que esta lista sea correcta, aunque probablemente lo sea. Esta función, y otras que no tienen argumentos, puede usarse en el modo de programación de plantillas (ver más adelante) usando la plantilla {:'approximate_formats()'}. Tenga en cuenta que los nombres de formato resultantes están siempre en mayúsculas, como en «EPUB». La función approximate_formats() es bastante más rápida que las funciones formats_... que se discuten más adelante.

author_links(val_separator, pair_separator) – devuelve un texto que contiene una lista de autores y los enlaces de dichos autores de la forma:

author1 val_separator author1_link pair_separator author2 val_separator author2_link etc.

Cada autor está separado de su correspondiente enlace por el texto val_separator, sin espacios adicionales. Los pares author:linkvalue están separados por el texto pair_separator, sin espacios adicionales. Es responsabilidad del usuario proporcionar separadores que no aparezcan en los nombres o enlaces de autor. El nombre de autor se incluye aunque su enlace esté vacío.

author_sorts(separador) – devuelve un texto que contiene la lista de valores de orden de autor para los autores del libro. El orden de autor es el que figura en los metadatos de información autores (diferente del valor de orden de autor del libro). La lista devuelta es de la forma orden_de_autor_1 separador orden_de_autor_2, etc. Los valores de orden de autor en la lista están en el mismo orden que los autores del libro. Si quiere espacios alrededor de separador, inclúyalos en el valor de separador.

book_count(consulta, usar_bv) – devuelve el número de libros encontrados al buscar consulta. Si usar_bv es 0 (cero), no se tienen en cuenta las bibliotecas virtuales. Esta función y su complemento book_values() son especialmente útiles en búsquedas de plantilla, permitiendo búsquedas que combinan la información de varios libros, como buscar series con un solo libro. No puede usarse en columnas compuestas, a no ser que que el ajuste allow_template_database_functions_in_composites sea True. Sólo puede usarse en la interfaz gráfica.

Por ejemplo, esta búsqueda de plantilla usa esta función y su complemento para encontrar todas las series con un solo libro:

Definir una plantilla guardada (usando Preferencias > Avanzado > Funciones de plantilla) llamada series_only_one_book (el nombre es arbitrario). La plantilla es:

program:
    vals = globals(vals='');
    if !vals then
        all_series = book_values('series', 'series:true', ',', 0);
        for series in all_series:
            if book_count('series:="' & series & '"', 0) == 1 then
                vals = list_join(',', vals, ',', series, ',')
            fi
        rof;
        set_globals(vals)
    fi;
    str_in_list(vals, ',', $series, 1, '')

La primera vez que se ejecuta esta plantilla (el primer libro que se comprueba) se almacenan los resultados de la búsqueda en la base de datos en una variable de plantilla global llamada vals. Estos resultados se usan para comprobar los siguientes libros sin volver a realizar la búsqueda.

Usar la plantilla guardada en una búsqueda de plantilla:

template:"program: series_only_one_book()#@#:n:1"

Al usar una plantilla guardada en lugar de poner la plantilla en la búsqueda. se evitan problemas causados por la necesidad de escapar las comillas en las expresiones de búsqueda.

book_values(columna, consulta, sep, usar_bv) – devuelve una lista de valores únicos contenidos en la columna columna (un nombre de consulta), separados por sep, de los libros encontrados al buscar consulta. Si usar_bv es 0 (cero), no se tienen en cuenta las bibliotecas virtuales. Esta función y su complemento book_count() son especialmente útiles en búsquedas de plantilla, permitiendo búsquedas que combinan la información de varios libros, como buscar series con un solo libro. No puede usarse en columnas compuestas, a no ser que que el ajuste allow_template_database_functions_in_composites sea True. Sólo puede usarse en la interfaz gráfica.

booksize() – devuelve el valor del campo «tamaño» de calibre. Devuelve '' si no hay formatos.

check_yes_no(nombre_de_campo, no_definido, es_false, es_true) – comprueba si el valor del campo de tipo sí o no identificado por el nombre de búsqueda nombre_de campo es uno de los valores especificado en los parámetros. Devuelve “”yes”` si coincide, y un texto vació en caso contrario. Establezca el parámetro no_definido, es_false o es_true a 1 (el número) para comprobar dicha condición, en caso contrario establézcalos a 0. Ejemplo:

check_yes_no("#bool", 1, 0, 1) devuelve 'Yes' si el campo de sí o no #bool es True o no definido (ni True ni False).

Más de uno de entre no_definido, es_false y es_true pueden ser 1.

ceiling(x) – devuelve el menor entero que es mayor o igual a x. Da un error si x no es un número.

character(nombre_carácter) – devuelve el carácter con nombre nombre_carácter. Por ejemplo, character('newline') devuelve un carácter de salto de línea ('\n'”). Los nombres de carácter admitidos son newline (salto de línea), return (retorno de carro), tab (tabulación) y backslash (barra invertida).

cmp(x, y, mn, ig, my) – compara x e y después de convertirlas en números. Devuelve mn si x <# y, ig si x ==# y y my en caso contrario. Esta función puede sustituirse normalmente por uno de los operadores de comparación numérica (==#, <#, >#, etc.).

connected_device_name(ubicación_de_almacenamiento) – si hay un dispositivo conectado, devuelve el nombre del dispositivo, en caso contrario devuelve un texto vacío. Cada ubicación de almacenamiento de un dispositivo tiene un nombre diferente. Los nombres de ubicación_de_almacenamiento son 'main', 'carda' y 'cardb'. Esta función sólo está disponible en la interfaz gráfica.

connected_device_uuid(ubicación_de_almacenamiento) – si un dispositivo está conectado, entonces devuelve el uuid (identificador único) del dispositivo, de lo contrario devuelve un texto vacío. Cada ubicación de almacenamiento de un dispositivo tiene un uuid diferente. Los nombres de ubicación_de_almacenamiento son 'main', 'carda' y 'cardb'. Esta función sólo está disponible en la interfaz gráfica.

current_library_name() – devuelve la última parte de la ruta a la biblioteca de calibre actual.

current_library_path() – devuelve la ruta completa a la biblioteca de calibre actual.

current_virtual_library_name() – devuelve el nombre de la biblioteca virtual actual si hay alguna, en caso contrario devuelve un texto vacío. Se conservan mayúsculas y minúsculas. Ejemplo: program: current_virtual_library_name(). Esta función sólo funciona en la interfaz gráfica.

date_arithmetic(fecha, cálculo, fmt) – Calcula una nueva fecha a partir de fecha usando cálculo. Devuelve la nueva fecha con el formato opcional fmt: si so se proporciona, el resultado estará en formato ISO. El argumento cálculo es un texto formado por pares vQ (valorQué) donde v es un número que puede ser negativo y Q es una de las letras siguientes:

s: añade v segundos a fecha

m: añade v minutos a fecha

h: añade v horas a fecha

d: añade v días a fecha

w: añade v semanas a fecha

y: añade v años a fecha, donde un año son 365 días.

Ejemplo: '1s3d-1m' añade 1 segundo, añade 3 días y resta 1 minuto a fecha.

days_between(fecha1, fecha2) – devuelve el número de días entre fecha1 y fecha2. El número es positivo si fecha1 es posterior a fecha2, en caso contrario es negativo. Si fecha1 o fecha2 no son fechas, la función devuelve un texto vacío.

divide(x, y) – devuelve x / y. Da un error si x o y no son números. Esta función puede sustituirse a menudo por el operador /.

eval(texto) – evalúa texto como un programa, pasando las variables locales. Esto permite usar el procesador de plantillas para elaborar resultados complejos a partir de variables locales. En el modo de programación de plantillas, dado que los caracteres { y } se interpretan antes de evaluar la plantilla, debe usarse [[ en lugar de { y ]] para }. Se convertirán automáticamente. Tenga en cuenta que los prefijos y sufijos (la sintaxis |prefijo|sufijo|) no pueden usarse en el argumento de esta función en el modo de programación de plantillas.

extra_file_size(nombre_archivo) – devuelve el tamaño en bytes del archivo adicional nombre_archivo en la carpeta data/ del libro si existe, o -1 en caso contrario. Ver también las funciones has_extra_files(), extra_file_names() y extra_file_modtime(). Esta función sólo puede usarse en la interfaz gráfica.

extra_file_modtime(nombre_archivo, texto_formato) – devuelve la fecha (y hora) de modificación del archivo adicional nombre_archivo en la carpeta data/ del libro si existe, o -1 en caso contrario. El formato de la fecha se especifica en texto_formato (ver format_date() para más detalles). Si texto_formato es un texto vacío, devuelve la fecha de modificación como un número real de segundos desde el inicio de la época. Ver también las funciones has_extra_files(), extra_file_names() y extra_file_size(). El inicio de la época depende del sistema operativo. Esta función sólo puede usarse en la interfaz gráfica.

extra_file_names(sep [, patrón]) devuelve una lista separada por sep de archivos adicionales en la carpeta data/ del libro. Si el argumento opcional patrón (una expresión regular) está presente, la lista contiene sólo archivos que coinciden con patrón. La coincidencia no distingue mayúsculas y minúsculas. Ver también las funciones has_extra_files(), extra_file_modtime() y extra_file_size(). Esta función sólo puede usarse en la interfaz gráfica.

field(nombre_de_búsqueda) – devuelve el campo de metadatos identificado por nombre_de_búsqueda.

field_exists(nombre_campo) – comprueba si existe un campo (columna) con el nombre de búsqueda nombre_campo, devuelve '1' si es así y un texto vacío si no.

finish_formatting(val, formato, prefijo, sufijo) – aplica el formato, prefijo y sufijo a un valor de la misma manera que se haría en una plantilla como {series_index:05.2f| - |- }. Esta función se proporciona para facilitar la conversión de plantillas complejas en modo de función única o de plantilla a plantillas MPG. Por ejemplo, el siguiente programa produce la misma salida que la plantilla anterior:

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Otro ejemplo: para la plantilla {series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} use:

program:
  strcat(
    re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
       finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
       field('title')
  )

first_matching_cmp(val, [ cmp, resultado, ]* otro_resultado) – compara val < cmp consecutivamente y devuelve el resultado correspondiente a la primera comparación que sea cierta. Devuelve otro_resultado si ninguna comparación es cierta. Ejemplo:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

devuelve "large". El mismo ejemplo con un primer valor de 16 devuelve "giant".

first_non_empty(valor [, valor]*) – devuelve el primer valor que no esté vacío. Si todos los valores están vacíos, se devuelve también un texto vacío. Puede incluir tantos valores como quiera.

floor(x) – devuelve el mayor entero que es menor o igual a x. Da un error si x no es un número.

format_date(val, formato_fecha) – da formato a un valor, que debe ser un texto de fecha, según formato_fecha y devuelve un texto. Los códigos de formato son:

d: el día del mes como un número sin cero a la izquierda (de 1 a 31)

dd: el día del mes como un número con cero a la izquierda (de 01 a 31)

ddd: el nombre del día de la semana traducido y abreviado (por ejemplo, de «lun» a «dom»)

dddd: el nombre del día de la semana completo traducido (por ejemplo, de «lunes» a «domingo»)

M: el mes como un número, sin cero a la izquierda (de 1 a 12)

MM: el mes como un número con cero a la izquierda (de 01 a 12)

MMM: el nombre del mes traducido y abreviado (por ejemplo, de «ene» a «dic»)

MMMM: el nombre del mes completo traducido (por ejemplo, de «enero» a «diciembre»)

yy: el año como un número de dos dígitos (de 00 a 99)

yyyy: el año como un número de cuatro dígitos.

h: la hora sin cero a la izquierda (de 0 a 11 o de 0 a 23, según am/pm)

hh: la hora con un cero a la izquierda (de 00 a 11 o de 00 a 23, según am7pm)

m: el minuto sin cero a la izquierda (de 0 a 59)

mm: el minuto con cero a la izquierda (de 00 a 59)

s: el segundo sin cero a la izquierda (de 0 a 59)

s: el segundo con cero a la izquierda (de 00 a 59)

ap: se usa un reloj de 12 horas en lugar de 24, con ap sustuido por el text traducido para «am» o «pm».

AP: se usa un reloj de 12 horas en lugar de 24, con AP sustuido por el text traducido para «AM» o «PM».

iso: la fecha con hora y zona horaria. Debe ser el único formato presente

to_number: convierte la fecha y hora en un número décimal (un «código de tiempo»)

from_number: convierte un número decimal (un «código de tiempo») en una fecha con formato iso. Si desea un formato diferente, añada el texto de formato desado depués de from_number y dos puntos (:). Ejemplo: from_number:dd MMM yyyy

Puede obtener resultados inesperados si la fecha a la que se da formato contiene nombres de meses traducidos, lo que puede ocurrir si ha modificado los ajustes de formato de fecha para incluir MMMM. En este caso, en lugar de usar la función field() como en:

format_date(field('pubdate'), 'yyyy')

use la función raw_field(), como en:

format_date(raw_field('pubdate'), 'yyyy')

format_date_field(nombre_campo, texto_formato) – da formato al valor en el campo nombre_campo, que debe ser el nombre de búsqueda de un campo de fecha, ya sea estándar o personalizado. Ver format_date() para los códigos de formato. Esta función es mucho más rápida que format_date y debe usarse cuando se esté dando formato al valor en un campo (columna). No puede usarse con fechas calculadas o variables de texto. Ejemplos:

format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')

formats_modtimes(formato_fecha) – devuelve una lista separada por comas de elementos separados por dos puntos (FMT:FECHA) que contienan las fechas de modificación para los formatos de un libro. El argumento formato_fecha especifica cómo se da formato a la fecha. Véase la función format_date para más detalles. Puede usar la función select para obtener la fecha de modificación de un formato específico. Tenga en cuenta que los nombres de formato siempre están en mayúsculas, como en «EPUB».

formats_paths() – devuelve una lista separada por comas de elementos separados por dos puntos (FMT:RUTA) que contienen la ruta completa a los formatos de un libro. Puede usar la función select para obtener la ruta a un formato específico. Tenga en cuenta que los nombres de formato están siempre en mayúsculas, como en «EPUB».

formats_sizes() – devuelve una lista separada por comas de elementos separados por dos puntos (FMT:TAMAÑO) que contienen los tamaños en bytes de los formatos de un libro. Puede usar la función select para obtener el tamaño de un formato específico. Tenga en cuenta que los nombres de formato siempre están en mayúsculas, como en «EPUB».

fractional_part(x) – devuelve el valor tras el punto decimal. Por ejemplo, fractional_part(3.14) devuelve 0.14. Da un error si x no es un número.

get_link(nombre_campo, valor_campo) – obtiene el enlace para el campo nombre_campo con el valor valor_campo. Si no hay ningún enlace asociado, devuelve un texto vacío. Ejemplos:

Lo siguiente devuelve el enlace asociado con la etiqueta Fiction:

get_link('tags', 'Fiction')

Esta plantilla hace una lista de los enlaces para todas las etiquetas asociadas con un libro, de la forma valor:enlace, ...:

program:
 ans = '';
 for t in $tags:
     l = get_link('tags', t);
     if l then
         ans = list_join(', ', ans, ',', t & ':' & get_link('tags', t), ',')
     fi
 rof;
 ans

has_cover() – devuelve 'Yes' si el libro tiene portada, en caso contrario devuelve un texto vacío.

has_extra_files([patrón]) devuelve el número de archivos adicionales o '' (un texto vacío). Si el argumento opcional patrón (una expresión regular) está presente, sólo se cuentan archivos que coinciden con patrón. La coincidencia no distingue mayúsculas y minúsculas. Ver también las funciones extra_file_names(), extra_file_modtime() y extra_file_size(). Esta función sólo puede usarse en la interfaz gráfica.

identifier_in_list(val, nombre_id [, val_encontrado, val_no_encontrado]) – trata val como una lista de identificadores separados por comas. Un identificador tiene el formato nombre_id:valor. El parámetro nombre_id es el texto de nombre de identificador a buscar, ya sea nombre_id o nombre_id:expreg. El primer caso coincide si hay algún identificador con ese nombre. El segundo caso coincide si algún identificador tiene ese nombre y la expresión regular coincide con el valor del identificador. Si se proporcionan val_encontrado y val_no_encontrado, si hay una coincidencia, se devuelve val_encontrado; de lo contrario, se devuelve val_no_encontrado. Si val_encontrado y val_no_encontrado están ausentes, si hay alguna coincidencia, se devuelve el par identificador:valor; de lo contrario, el texto vacío ('').

is_marked() – comprueba si el libro está «marcado» en calibre, Si lo está, devuelve el valor de la marca, bien 'true' (en minúsculas) o bien una lista separada por comas de los nombres de marca. Devuelve '' (un texto vacío) si el libro no está marcado. Esta función sólo está disponible en la interfaz gráfica.

language_codes(nombres) – devuelve los códigos de idioma [https://www.loc.gov/standards/iso639-2/php/code_list.php] correspondientes a nombres. Los nombres deben estar en el idioma actual. El argumento nombres es una lista separada por comas.

list_contains(valor, separador, [ patrón, encontrado, ]* no_encontrado) – (Sinónimo de in_list) Interpreta el campo como una lista de elementos separados por separador y evalúa patrón con cada valor de la lista. Si patrón coincide con alguno de los elementos de la lista, devuelve encontrado, en caso contrario devuelve no_encontrado. Los argumentos patrón y encontrado pueden repetirse tantas veces como se desee, lo que permite devolver diferentes valores según la búsqueda. Los patrones se evalúan en orden. Se devuelve la primera coincidencia. Sinónimos: in_list(), list_contains()

list_count(valor, separador) – interpreta valor como una lista de elementos separados por ``separador`` y devuelve el número de elementos de la lista. Sinónimos: count(), list_count()

list_count_matching(lista, patrón, separador) – interpreta lista como una lista de elementos separados por `separador` y devuelve el número de elementos de la lista que coinciden con la expresión regular patrón. Sinónimos: list-count_matching(), count_matching()

list_difference(lista1, lista2, separador) – devuelve una lista construida eliminando de lista1 cualquier elemento que aparezca en lista2, sin distinguir mayúsculas y minúsculas. Los elementos de lista1 y lista2 están separados por separador, así como los de la lista resultante.

list_equals(lista1, sep1, lista2, sep2, val_sí, val_no) – devuelve val_sí si lista1 y lista2 contienen los mismos elementos, en caso contrario devuelve val_no. Los elementos se determinan dividiendo cada lista por el correspondiente carácter separador (sep1 o sep2). El orden de los elementos no es relevante. La comparación no diferencia mayúsculas y minúsculas.

list_intersection(lista1, lista2, separador) – devuelve una lista construida eliminando de lista1 cualquier elemento que no aparezca en lista2, sin distinguir mayúsculas y minúsculas. Los elementos de lista1 y lista2 están separados por separador, así como los de la lista resultante.

list_join(con_separador, lista1, separador1 [, lista2, separador2]*) – devuelve una lista formada por unión de los elementos en las listas de origen (lista1, etc.) usando con_separador entre los elementos de la lista resultante. Los elementos de cada lista de origen lista[123...] están separador por el correspondiente separador[123...]. Una lista puede contener cero elementos. Puede ser un campo como publisher que contiene un solo valor, una lista de un elemento en la práctica. Los duplicados se eliminan sin distinción de mayúsculas y minúsculas. Los elementos se devuelven en el orden en que aparecen en las listas de origen. Si los elementos difieren sólo en mayúsculas y minúsculas, se usa la última aparición. Todos los separadores pueden ser de más de un carácter.

Ejemplo:

program:
  list_join('#@#', $authors, '&', $tags, ',')

Puede usar list_join sobre los resultados de ejecuciones previas de list_join de la siguiente manera:

program:
  a = list_join('#@#', $authors, '&', $tags, ',');
  b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

Puede usar expresiones para generar una lista. Por ejemplo, supongamos que quiere los elementos de authors y #genre, pero con estos últimos reemplazados por la palabra ``Genre: `` seguida de la primera letra del elemento, es decir con «Ficción» convertido en «Genre: F». Esto se consigue con lo siguiente:

program:
  list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'),  ',')

list_re(lista_orig, separador, incluir, sust_opc) – construye una lista separando primero lista_orig en elementos usando el carácter separador. Para cada elemento en la lista, comprueba si coincide con la expresión regular incluir. Si coincide, se añade a la lista final. Si sust_opc no es un texto vacío, se aplica la sustitución antes de añadir el elemento a la lista final.

list_re_group(lista_orig, separador, incluir, búsqueda [, plantilla_grupo]*) – como list_re, pero las sustituciones no son opcionales. Usa re_group(elemento, búsqueda, plantilla_grupo ...) para hacer sustituciones..

list_remove_duplicates(lista, separador) – devuelve una lista construida por eliminación de los duplicados en lista. No distingue mayúsculas y minúsculas, se mantiene la última versión. Los elementos de lista están separados por separador, así como los de la lista resultante.

list_sort(lista, dirección, separador) – devuelve lista ordenada lexicográficamente sin distinción de mayúsculas y minúsculas. Si dirección es cero, la lista se ordena de manera ascendente, en caso contrario, de manera descendente. Los elementos de lista están separados por separador, así como los de la lista resultante.

list_split(lista, sep, prefijo_id) – divide la lista en valores separados por sep y asigna los valores a variables locales con nombre prefijo_id_N, donde N es la posición del valor en la lista. El primer elemento tiene posición 0 (cero). La función devuelve el último elemento de la lista.

Ejemplo:

list_split('one:two:foo', ':', 'var')

es equivalente a:

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

list_union(lista1, lista2, separador) – devuelve una lista construida por combinación de los elementos en lista1 y lista2, eliminando los duplicados de lista2 (no distingue mayúsculas y minúsculas, se mantiene la versión de lista1). Los elementos de lista1 y lista2 están separados por separador, así como los de la lista resultante. Sinónimos: merge_list(), list_union()

mod(x, y) – devuelve floor del resto de x / y. Da un error si x o y no son números.

multiply(x [, y]*) – devuelve el producto de sus argumentos. Da un error si algún argumento no es un número. Esta función puede sustituirse a menudo por el operador *.

not(valor) – devuelve el texto '1' si el valor está vacío, en caso contrario, devuelve un texto vacío. Esta función puede sustituirse a menudo por el operador de negación unario (!).

ondevice() – devuelve el texto 'Yes' si ondevice está activado, si no, devuelve un texto vacío.

or(valor [, valor]*) – devuelve el texto '1' si alguno de los valores no está vacío, en caso contrario, devuelve un texto vacío. Pueden incluirse tantos valores como se desee. Esta función puede sustituirse a menudo por el operador ||. Un motivo para no sustituirla es si la evaluación en cortocircuito puede cambiar los resultados debido a efectos secundarios.

print(a [, b]*) – escribe los argumentos en la salida estándar. Sólo será visible si inicia calibre a partir de línea de órdenes (calibre-debug -g). La función print siempre devuelve su primer argumento.

range(inicio, fin, paso, límite) – devuelve una lista de números generados por iteración en el intervalo especificado por los argumentos inicio, fin y paso, con una longitud máxima de límite. El primer elemento generado es inicio. Los valores siguientes son valor_sig = valor_act + paso. El proceso continúa mientras valor_sig < fin, suponiendo que paso sea positivo, en caso contrario mientras valor_sig > fin. Si inicio no cumple la condición, es decir si inicio >= fin (en caso de que paso sea positivo), se devuelve una lista vacía. El límite establece la longitud máxima de la lista y tiene un valor predeterminado de 1000. Los argumentos inicio, paso y límite son opcionales. Si se ejecuta range() con un argumento, se interpreta como fin. Dos argumentos especifican inicio y fin. Tres argumentos especifican inicio, fin y paso. Cuatro argumentos especifican inicio, fin, paso y límite. Ejemplos:

range(5) -> '0, 1, 2, 3, 4'
range(0, 5) -> '0, 1, 2, 3, 4'
range(-1, 5) -> '-1, 0, 1, 2, 3, 4'
range(1, 5) -> '1, 2, 3, 4'
range(1, 5, 2) -> '1, 3'
range(1, 5, 2, 5) -> '1, 3'
range(1, 5, 2, 1) -> error(limit exceeded)

raw_field(nombre_de_búsqueda [, predeterminado_opcional]) – devuelve el campo de metadatos nombrado por nombre_de_búsqueda sin aplicar ningún formato. Evalúa y devuelve el segundo argumento opcional predeterminado_opcional si el campo no está definido (None).

raw_list(nombre_de_búsqueda, separador) – devuelve la lista de metadatos nombrada por nombre_de_búsqueda sin aplicar ningún formato ni ordenación, con los elementos separados por separador.

re_group(valor, patrón [, plantilla_para_grupo]*) – devuelve un texto formado por aplicación de la expresión regular patrón a valor, sustituyendo cada coincidencia con el texto devuelto por la correspondiente plantilla. En el modo de programación de plantillas, como en las funciones template y eval, debe usarse [[ en lugar de { y ]] en lugar de }.

El siguiente ejemplo busca una serie con más de una palabra y pone la primera en mayúsculas:

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

round(x) – devuelve el entero más cercano a x. Da un error si x no es un número.

series_sort() – devuelve el valor de orden de serie.

strcat(a [, b]*) – puede tomar cualquier número de argumentos. Devuelve texto formado por la concatenación de todos los argumentos.

strcat_max(máx, texto1 [, prefijo2, texto2]*) – devuelve un texto formado por concatenación de los argumentos. El valor devuelto es inicialmente texto1. Se van añadiendo textos formados por parejas prefijo, texto al final del valor mientras la longitud del resultado sea menor que máx. Los prefijos pueden estar vacíos. El resultado es siempre al menos texto1, aunque su longitud sea mayor que máx. Pueden especificarse tantas parejas prefijo, texto como se desee.

strcmp(x, y, mn, ig, my) – hace una comparación lexicográfica sin distinción de mayúsculas y minúsculas entre x e y como textos. Devuelve mn si x < y, ig si x == y, my en otros casos. Esta función puede sustituirse a menudo por los operadores de comparación lexicográfica (==, >, <, etc.)

strcmpcase(x, y, mn, ig, my) – hace una comparación con distinción de mayúsculas y minúsculas entre x e y. Devuelve mn si x < y. Devuelve ig si x = y, o my en caso contrario.

Atención: Éste NO es el comportamiento predeterminado usado por calibre, por ejemplo, en los operadores de comparación léxica (==, >, <, etc.). Esta función podría dar resultados inesperados, use preferentemente strcmp() siempre que sea posible.

strlen(valor) – Devuelve la longitud del texto valor.

substr(texto, inicio, fin) – devuelve los caracteres entre la posición inicio y fin de texto. El primer carácter de texto está en la posición cero. Si fin es negativo, entonces indica la posición contando desde la derecha. Si fin es cero, indica el último carácter. Por ejemplo, substr('12345', 1, 0) devuelve 2345, y substr('12345', 1, -1) devuelve 234.

subtract(x, y) – devuelve x - y. Da un error si x o y no son números. Esta función puede sustituirse a menudo por el operador -.

switch_if([expresión_de prueba, expresión_de_valor,]+ otra_expresión) – para cada par expresión_de_prueba, expresión_de_valor, comprueba si expresión_de_prueba es True (no vacía) y, de ser así, devuelve el resultado de expresión_de_valor. Si ninguna expresión_de_prueba es True, se devuelve el resultado de otra_expresión. Puede haber tantos pares expresión_de_prueba, expresión_de_valor como se desee.

today() – devuelve un texto para la fecha y hora hoy (ahora). Este valor está preparado para usarse con format_date o days_between, pero puede manipularse como cualquier otro texto. La fecha está en formato ISO [https://en.wikipedia.org/wiki/ISO_8601].

template(x) – evalúa x como una plantilla. La evaluación se hace en su propio contexto, lo que significa que las variables no comparten entre quien llama a la función y la evaluación de la plantilla.

to_hex(val) – devuelve el texto val codificado en hexadecimal. Esto es útil al construir URL de calibre.

urls_from_identifiers(identificadores, ordenar_resultado) – dada una lista de identificadores separados por comas, donde cada identificador es un par de valores separados por dos puntos (nombre_id:valor_id), devuelve una lista de URL HTML separados por comas, generados a partir de los identificadores. La lista no está ordenada si ordenar_resultado es 0 (carácter o número), en caso contrario se ordena alfabéticamente según el nombre del identificador. Los URL se generan de la misma manera que la columna predefinida de identificadores cuando se muestra en Detalles del libro.

Programas más complejos en expresiones de plantilla: modo de programación de plantillas

El modo de programación de plantillas (MPP) es una mezcla entre el modo de programación general y el modo de función única. El MPP se diferencia del modo de función única en que permite escribir expresiones de plantilla que hacen referencia a otros campos de metadatos, usar funciones anidadas, modificar variables y hacer operaciones aritméticas. Se diferencia del modo de programación general en que la plantilla está entre caracteres { y } y no empieza por la palabra program. La parte del programa de la plantilla es una expression_list del modo de programación general.

Un ejemplo: supongamos que quiere una plantilla que muestre la serie de un libro si la tiene, y en caso contrario muestre el valor del campo personalizado #genre. Esto no se puede hacer en el modo de función única, porque no se puede hacer referencia a otro campo en la expresión de la plantilla. En el MPP sí se puede. La siguiente expresión lo demuestra:

{#series:'ifempty($, field('#genre'))'}

El ejemplo muestra varias cosas:

El MPP se usa si la expresión empieza por :' y termina en '}. Cualquier otra cosa se supone que corresponde al modo de función única.

la variable $ representa el campo nombrado en la plantilla sobre la que opera la expresión, #series en este caso.

las funciones deben llevar todos sus argumentos. No hay valores predeterminados. Por ejemplo, las funciones estándar predefinidas deben tener un parámetro inicial que indique el campo de origen.

los espacios en blanco se ignoran y se pueden utilizar en cualquier lugar dentro de la expresión.

los textos constantes se encierran en comillas del mismo tipo, ya sea ' o ".

Todas las funciones descritas en el modo de función única y en el modo de programación general pueden usarse en el MPP.

En el MPP, usar los caracteres { y } en textos literales puede conducir a errores o resultados inesperados porque confunden al procesador de plantillas. Éste intenta interpretarlos como límites de expresiones de plantilla y no como caracteres. En algunos casos, pero no todos, puede sustituir un { por [[ y un } por ]]. En general, si un program contiene caracteres { y }, debería usar el modo de programación general.

Como en el modo de programación general, para las funciones descritas en el modo de función única debe proporcionar el valor sobre el que opera la función como primer argumento, además de los argumentos descritos. En el MPP puede usar $ para acceder al valor especificado por el nombre de búsqueda de la expresión de plantilla.

Modo de plantilla de Python

El modo de plantilla de Python (MPPy) le permite escribir plantillas usando el lenguaje Python nativo y la interfaz de programación de calibre [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. La interfaz de programación de base de datos será la más útil, pero en este manual no se discutirán más detalles. Las plantillas MPPy son más rápidas y pueden realizar operaciones más complicadas, pero debe saber escribir código Python usando la interfaz de programación de calibre.

Una plantilla MPPy empieza por:

python:
def evaluate(book, context):
    # book is a calibre metadata object
    # context is an instance of calibre.utils.formatter.PythonTemplateContext,
    # which currently contains the following attributes:
    # db: a calibre legacy database object.
    # globals: the template global variable dictionary.
    # arguments: is a list of arguments if the template is called by a GPM template, otherwise None.
    # funcs: used to call Built-in/User functions and Stored GPM/Python templates.
    # Example: context.funcs.list_re_group()

    # your Python code goes here
    return 'a string'

Puede añadir el texto anterior a la plantilla usando el menú contextual, al que normalmente se accede pulsando el botón derecho. Los comentarios no son importantes y pueden eliminarse. Debe usar la sangría de Python.

El objeto de contexto admite str(context), que devuelve un texto con el contenido del contexto y context.attributes, que devuelve una lista con los nombres de los atributos del contexto.

El atributo context.funcs le permite invocar funciones incorporadas y de usuario, así como plantillas guardadas MPG o Python, de manera que puede ejecutarlas directamente en el código. Las funciones se identifican por sus nombres. Si el nombre entra en conflicto con una palabra clave de Python, añada un guión bajo al final. Ejemplos:

context.funcs.list_re_group()
context.funcs.assert_()

He aquí un ejemplo de una plantilla MPPy que genera una lista de todos los autores de una serie. La lista se guarda en una Columna generada a partir de otras columnas, funciona como etiquetas. Se muestra en los detalles del libro y tiene la opción en líneas separadas activada (en Preferencias > Apariencia > Detalles del libro). Esta opción requiere que la lista esté separada por comas. Para satisfacer este requisito la plantilla convierte las comas en los nombres de autor a punto y coma y luego construye una lista de autores separados por comas. Los autores se ordenan, razón por la cual la plantilla usa author_sort.

python:
def evaluate(book, context):
    if book.series is None:
        return ''
    db = context.db.new_api
    ans = set()
    # Get the list of books in the series
    ids = db.search(f'series:"={book.series}"', '')
    if ids:
        # Get all the author_sort values for the books in the series
        author_sorts = (v for v in db.all_field_for('author_sort', ids).values())
        # Add the names to the result set, removing duplicates
        for aus in author_sorts:
            ans.update(v.strip() for v in aus.split('&'))
    # Make a sorted comma-separated string from the result set
    return ', '.join(v.replace(',', ';') for v in sorted(ans))

La salida en los detalles del libro tiene este aspecto:

Plantillas guardadas

El modo de programación general y el modo de plantilla de Python permiten guardar plantillas e invocarlas desde otra plantilla, casi como invocar funciones guardadas. Puede guardar plantillas en Preferencias > Avanzado > Funciones de plantilla. Hay más información en el diálogo correspondiente. Puede invocar una plantilla de la misma manera que una función, pasando argumentos por posición si lo desea. Un argumento puede ser una expresión. Ejemplos para invocar una plantilla, suponiendo que la plantilla guardada se llame foo:

lala() – invoca la plantilla sin pasar argumentos.

lala(a, b) – invoca la plantilla pasando los valores de las variables a y b.

lala(if field('series') then field('series_index') else 0 fi) – si el libro tiene series, pasa el valor de series_index, en caso contrario pasa el valor 0.

En el MPG, puede obtener el valor de los argumentos pasados en la invocación de la plantilla guardada usando la función arguments. Esta función declara e inicializa las variables locales, parámetros en la práctica. Las variables son posicionales: obtienen el valor del argumento usado en la misma posición en la invocación. Si el argumento correspondiente no aparece en la invocación, arguments asigna a esa variable el valor predeterminado suministrado. Si no hay valor predeterminado, será un texto vacío. Por ejemplo, la siguiente función arguments declara dos variables: key y ``alternate`:

arguments(key, alternate='series')

Ejemplos, suponiendo de nuevo que la plantilla guardada se llame lala:

lala('#miserie') – a la variable key se le asigna el valor '#miserie' y a la variable alternate se le asigna el valor predeterminado 'serie'.

lala('series', '#genero') a la variable key se le asigna el valor 'series' y a la variable alternate se le asigna el valor '#genero'.

lala() – a la variable key se le asigna un texto vacío y a la variable alternate se le asigna el valor predeterminado 'series'.

En el MPPy los argumentos se pasan en el parámetro arguments, que es una lista de textos. No existe la posibilidad de especificar valores predeterminados. Debe comprobar la longitud de la lista arguments para asegurarse de que el número de argumentos es el esperado.

Una buena manera de probar las plantillas guardadas es usar el diálogo Prueba de plantillas. Para facilitar su acceso, asígnele un atajo de teclado en Preferencias > Avanzado > Atajos > Prueba de plantillas. Si le asigna un atajo de teclado podrá alternar rápidamente entre la prueba de plantillas y la modificación del código de programa de la plantilla.

Pasar información adicional a las plantillas

Un desarrollador puede decidir pasar información adicional al procesador de plantillas, como metadatos del libro específicos para una aplicación o información sobre lo que se quiere que haga el procesador. Una plantilla puede acceder a esta información y usarla durante la evaluación.

Desarrollador: cómo pasar información adicional

La información adicional es un diccionario Python que contiene pares nombre_de_variable: valor de variable donde los valores deberían ser textos. La plantilla puede acceder al diccionario, creando variables locales con nombre nombre_de_variable y valor valor de variable. El usuario no puede cambiar el nombre, así que lo mejor es usar nombres que no entren en conflicto con otras variables locales, por ejemplo añadiendo un guión bajo al principio del nombre.

El diccionario se pasa al procesador de plantillas (el formatter) usando el argumento global_vars=diccionario. La forma completa del método es:

def safe_format(self, fmt, kwargs, error_value, book,
                column_name=None, template_cache=None,
                strip_results=True, template_functions=None,
                global_vars={})

Creador de plantillas: cómo acceder a la información adicional

Puede acceder a la información adicional (el diccionario globals) en una plantilla usando la función de plantilla:

globals(id[=expression] [, id[=expression]]*)

donde id es cualquier nombre de variable permitido. Esta función comprueba si la información adicional prevista por el desarrollador contiene el nombre. Si es así, la función asigna el valor a una variable local con ese nombre. Si el nombre no está presente en la información adicional y se suministra una expresión, se evalua dicha expresión y el resultado se asigna a la variable local. Si no existe ni un valor ni una expresión, la función asigna el texto vacío ('') a la variable local.

Una plantilla puede establecer un valor en el diccionario globals usando la función de plantilla:

set_globals(id[=expression] [, id[=expression]]*)

Esta función asigna la pareja id:valor del diccionario globals, donde valor es el valor de la variable local de la plantilla id. Si la variable no existe, valor se toma como el resultado de evaluar expresión.

Notas sobre las diferencias entre modos

Los tres modos de programación, el modo de función única (MFU), el modo de programación de plantillas (MPP) y el modo de programación general (MPG) funcionan de distinta manera. MFU pretende ser «sencillo» y oculta muchos elementos del lenguaje de programación.

Diferencias:

En el MFU el valor de la columna se pasa siempre como un primer argumento «invisible» a la función que se incluye en la plantilla.

El MFU no hace distinciones entre variables y textos; todos los valores son textos.

La siguiente plantilla MFU devuelve el nombre de la serie o el texto 'no series':

{series:ifempty(no series)}

La plantilla equivalente en MPP es:

{series:'ifempty($, 'no series')'}

La plantilla equivalente en MPG es:

program: ifempty(field('series'), 'no series')

El primer argumento de ifempty es el valor del campo series. El segundo argumento es el texto no series. En MFU el primer argumento, el valor, se pasa automáticamente (es el argumento invisible).

Varias funciones de plantilla, como booksize() y current_library_name(), no admiten argumentos. Debido al «argumento invisible», no puede usar estas funciones en MFU.

Las funciones anidadas, en las que una función invoca a otra función para evaluar un argumento, no pueden usarse en MFU. Por ejemplo, esta plantilla, que pretende devolver los 5 primeros caracteres del valor de la serie convertidos en mayúsculas, no funcionará en MFU:

{series:uppercase(substr(0,5))}

MPP y MPG permiten usar funciones anidadas. La plantilla anterior en MPP sería:

{series:'uppercase(substr($, 0,5))'}

En MPG sería:

program: uppercase(substr(field('series'), 0,5))

Como se mencionó ateriormente en la sección modo de programación de plantillas, en el MPP, usar los caracteres { y } en textos literales puede conducir a errores o resultados inesperados porque confunden al procesador de plantillas. Éste intenta interpretarlos como límites de plantilla y no como caracteres. En algunos casos, pero no todos, puede sustituir un { por [[ y un } por ]]. En general, si un program contiene caracteres { y }, debería usar el modo de programación general.

Funciones de plantilla Python definidas por el usuario

Puede añadir funciones propias al procesador de plantillas. Dichas funciones pueden usarse en cualquiera de los tres modos de programación de plantillas. Las funciones se añaden en Preferencias > Avanzado > Funciones de plantilla. Las instrucciones se muestran en el correspondiente cuadro de diálogo.

Notas especiales para plantillas de guardado o envío

Cuando una plantilla se usa como plantilla de «guardado a disco» o de «envío a dispositivo», ocurre un procesado especial. Los valores de los campos se sanean, eliminando caracteres especiales para los sistemas operativos por guiones bajos, incluyendo barras. Esto significa que el texto de los campos no puede usarse para crear carpetas. Sin embargo, las barras no se modifican en los textos de prefijo o sufijo, por lo que las barras en estos textos harán que se creen carpetas. Gracias a esto, es posible crear estructuras de carpetas de profundidad variable.

Por ejemplo, supongamos que quiere una estructura de carpetas serie/índice de serie - título, con la salvedad de que si la serie no existe el título debe estar en la carpeta superior. La plantilla para conseguir esto es:

{series:||/}{series_index:|| - }{title}

La barra y el guión sólo aparecen si la serie no está vacía.

La función lookup() nos permite realizar un procesado aún más complejo. Por ejemplo, supongamos que si un libro tiene una serie, entonces queremos una estructura de carpetas serie/índice de serie - título. Si el libro no tiene una serie, entonces queremos la estructura género/orden de autor/título. Si el libro no tiene género, queremos que use «Desconocido». Queremos seguir dos caminos completamente distintos según el valor de la serie.

Para lograr esto:

Creamos un campo compuesto (démosle el nombre de consulta #aa) que contiene {series}/{series_index} - {title}. Si la serie no está vacía, esta plantilla produce serie/número_de_serie - título.

Creamos un campo compuesto (démosle el nombre de consulta #bb) que contenga {#genre:ifempty(Desconocido)}/{author_sort}/{title}. Esta plantilla produce género/orden de autor/título, donde un género vacío se sustituye por Desconocido.

Establecemos la plantilla de guardado en {series:lookup(.,#aa,#bb)}. Esta plantilla elige el campo compuesto #aa si la serie no está vacía y el campo compuesto #bb si la serie está vacía. Obtenemos por lo tanto dos rutas de guardado completamente diferentes según el campo series esté o no vacío.

Consejos

Use la prueba de plantillas para probar plantillas. Añada esta función al menú contextual para libros en lea biblioteca o asígnele un atajo de teclado.

Las plantillas pueden usar otras plantillas haciendo referencia a columnas compuestas construidas con la plantilla deseada. También puede usar plantillas guardadas.

En un panel de conexiones, puede establecer un campo como vacío (o lo que sea equivalente a vacío) utilizando la plantilla especial {}. Esta plantilla siempre producirá un texto vacío.

La técnica descrita anteriormente para mostrar los números incluso si son cero funciona con el campo estándar series_index.

Referencia de funciones

Reference for all built-in template language functions

Arithmetic

add(x [, y]*)

ceiling(x)

divide(x, y)

floor(x)

fractional_part(x)

mod(x)

multiply(x [, y]*)

round(x)

subtract(x, y)

Boolean

and(valor [, valor]*)

not(valor)

or(valor [, valor]*)

Date functions

date_arithmetic(fecha, cálculo, fmt)

days_between(fecha1, fecha2)

today()

Formatting values

finish_formatting(val, fmt, prefijo, sufijo)

format_date(val, texto_formato)

format_date_field(nombre_campo, texto_formato)

format_number(v, plantilla)

human_readabe(v)

rating_to_stars(valor, usar_media_estrella)

urls_from_identifiers(identificadores, ordenar_resultado)

Get values from metadata

annotation_count()

approximate_formats()

author_links(sep_val, sep_par)

author_sorts(separador)

booksize()

connected_device_name(ubicación_de_almacenamiento)

connected_device_uuid(ubicación_de_almacenamiento)

current_library_name()

current_library_path()

current_virtual_library_name()

field(nombre_de_búsqueda)

formats_modtimes(formato_fecha)

formats_paths()

formats_sizes()

has_cover()

is_marked()

language_codes(nombres)

language_strings(códigos, localizar)

ondevice()

raw_field(nombre_de_búsqueda [, predeterminado_opcional])

raw_list(nombre_de_búsqueda, separador)

series_sort()

user_categories()

virtual_libraries()

If-then-else

check_yes_no(nombre_de_campo, no_definido, es_false, es_true)

contains(val, patrón, texto_si_coincide, texto_si_no_coincide)

field_exists(nombre_campo)

ifempty(val, texto_si_vacío)

test(val, texto_si_no_vacío, texto_si_vacío)

Iterating over values

first_non_empty(valor [, valor]*)

lookup(val, [patrón, campo,]+ otro_campo)

switch(val, [patrón, valor,]+ otro_valor)

switch_if([expresión_de prueba, expresión_de_valor,]+ otra_expresión)

List lookup

identifier_in_list(val, nombre_id [, val_encontrado, val_no_encontrado])

in_list(val, separador, [patrón, encontrado,]+ no_encontrado)

list_item(val, índice, separador)

select(val, clave)

str_in_list(val, separador, [texto, encontrado,]+ no_encontrado)

List manipulation

count(val, separador)

list_count_matching(lista, patrón, separador)

list_difference(lista1, lista2, separador)

list_equals(lista1, sep1, lista2, sep2, val_sí, val_no)

list_intersection(lista1, lista2, separador)

list_join(con_separador, lista1, separador1 [, lista2, separador2]*)

list_re(lista_orig, separador, incluir, sust_opc)

list_re_group(list_orig, separador, incluir, búsqueda [, plantilla_grupo]+)

list_remove_duplicates(lista, separador)

list_sort(lista, dirección, separador)

list_split(lista, sep, prefijo_id)

list_union(lista1, lista2, separador)

range(inicio, fin, paso, límite)

subitems(val, índice_inicio, índice_fin)

sublist(val, índice_inicio, índice_fin, separador)

Other

arguments(id[=expresión] [, id[=expresión]]*)

assign(id, val)

globals(id[=expresión] [, id[=expressión]]*)

print(a [, b]*)

Recursion

eval(plantilla)

Relational

cmp(x, y, mn, ig, my)

first_matching_cmp(val [, cmp1, resultado1]+, otro_resultado)

strcmp(x, y, mn, ig, my)

strcmpcase(x, y, mn, ig, my)

String case changes

capitalize(val)

lowercase(val)

titlecase(val)

uppercase(val)

String manipulation

character(nombre_carácter)

re(val, patrón, sustitución)

re_group(val, patrón [, plantilla_para_grupo]*)

shorten(val, car_izq, texto_medio, car_der)

strcat(a [, b]*)

strcat_max(máx, texto1 [, prefijo2, texto2]*)

strlen(a)

substr(texto, inicio, fin)

swap_around_articles(valor, separador)

swap_around_comma(val)

to_hex(val)

transliterate(a)

Template database functions

book_count(consulta, usar_bv)

book_values(columna, consulta, sep, usar_bv)

extra_file_size(nombre_archivo)

get_link(nombre_campo, valor_campo)

get_note(nombre_campo, valor_campo, texto)

has_note(nombre_campo, valor_campo)

other

set_globals(id[=expresión] [, id[=expresión]]*)

API of the Metadata objects

Metadata

STANDARD_METADATA_FIELDS

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

El lenguaje de plantillas de calibre »

Reference for all built-in template language functions

Reference for all built-in template language functions

Here, we document all the built-in functions available in the calibre template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.

Arithmetic

add(x [, y]*)

ceiling(x)

divide(x, y)

floor(x)

fractional_part(x)

mod(x)

multiply(x [, y]*)

round(x)

subtract(x, y)

Boolean

and(valor [, valor]*)

not(valor)

or(valor [, valor]*)

Date functions

date_arithmetic(fecha, cálculo, fmt)

days_between(fecha1, fecha2)

today()

Formatting values

finish_formatting(val, fmt, prefijo, sufijo)

format_date(val, texto_formato)

format_date_field(nombre_campo, texto_formato)

format_number(v, plantilla)

human_readabe(v)

rating_to_stars(valor, usar_media_estrella)

urls_from_identifiers(identificadores, ordenar_resultado)

Get values from metadata

annotation_count()

approximate_formats()

author_links(sep_val, sep_par)

author_sorts(separador)

booksize()

connected_device_name(ubicación_de_almacenamiento)

connected_device_uuid(ubicación_de_almacenamiento)

current_library_name()

current_library_path()

current_virtual_library_name()

field(nombre_de_búsqueda)

formats_modtimes(formato_fecha)

formats_paths()

formats_sizes()

has_cover()

is_marked()

language_codes(nombres)

language_strings(códigos, localizar)

ondevice()

raw_field(nombre_de_búsqueda [, predeterminado_opcional])

raw_list(nombre_de_búsqueda, separador)

series_sort()

user_categories()

virtual_libraries()

If-then-else

check_yes_no(nombre_de_campo, no_definido, es_false, es_true)

contains(val, patrón, texto_si_coincide, texto_si_no_coincide)

field_exists(nombre_campo)

ifempty(val, texto_si_vacío)

test(val, texto_si_no_vacío, texto_si_vacío)

Iterating over values

first_non_empty(valor [, valor]*)

lookup(val, [patrón, campo,]+ otro_campo)

switch(val, [patrón, valor,]+ otro_valor)

switch_if([expresión_de prueba, expresión_de_valor,]+ otra_expresión)

List lookup

identifier_in_list(val, nombre_id [, val_encontrado, val_no_encontrado])

in_list(val, separador, [patrón, encontrado,]+ no_encontrado)

list_item(val, índice, separador)

select(val, clave)

str_in_list(val, separador, [texto, encontrado,]+ no_encontrado)

List manipulation

count(val, separador)

list_count_matching(lista, patrón, separador)

list_difference(lista1, lista2, separador)

list_equals(lista1, sep1, lista2, sep2, val_sí, val_no)

list_intersection(lista1, lista2, separador)

list_join(con_separador, lista1, separador1 [, lista2, separador2]*)

list_re(lista_orig, separador, incluir, sust_opc)

list_re_group(list_orig, separador, incluir, búsqueda [, plantilla_grupo]+)

list_remove_duplicates(lista, separador)

list_sort(lista, dirección, separador)

list_split(lista, sep, prefijo_id)

list_union(lista1, lista2, separador)

range(inicio, fin, paso, límite)

subitems(val, índice_inicio, índice_fin)

sublist(val, índice_inicio, índice_fin, separador)

Other

arguments(id[=expresión] [, id[=expresión]]*)

assign(id, val)

globals(id[=expresión] [, id[=expressión]]*)

print(a [, b]*)

Recursion

eval(plantilla)

Relational

cmp(x, y, mn, ig, my)

first_matching_cmp(val [, cmp1, resultado1]+, otro_resultado)

strcmp(x, y, mn, ig, my)

strcmpcase(x, y, mn, ig, my)

String case changes

capitalize(val)

lowercase(val)

titlecase(val)

uppercase(val)

String manipulation

character(nombre_carácter)

re(val, patrón, sustitución)

re_group(val, patrón [, plantilla_para_grupo]*)

shorten(val, car_izq, texto_medio, car_der)

strcat(a [, b]*)

strcat_max(máx, texto1 [, prefijo2, texto2]*)

strlen(a)

substr(texto, inicio, fin)

swap_around_articles(valor, separador)

swap_around_comma(val)

to_hex(val)

transliterate(a)

Template database functions

book_count(consulta, usar_bv)

book_values(columna, consulta, sep, usar_bv)

extra_file_size(nombre_archivo)

get_link(nombre_campo, valor_campo)

get_note(nombre_campo, valor_campo, texto)

has_note(nombre_campo, valor_campo)

other

set_globals(id[=expresión] [, id[=expresión]]*)

API of the Metadata objects

Arithmetic

add(x [, y]*)

class calibre.utils.formatter_functions.BuiltinAdd[fuente]

add(x [, y]*) – devuelve la suma de sus argumentos. Da un error si algún argumento no es un número. Esta función puede sustituirse a menudo por el operador +.

ceiling(x)

class calibre.utils.formatter_functions.BuiltinCeiling[fuente]

ceiling(x) – devuelve el menor entero que es mayor o igual a x. Da un error si x no es un número.

divide(x, y)

class calibre.utils.formatter_functions.BuiltinDivide[fuente]

divide(x, y) – devuelve x / y. Da un error si «x» o «y» no son números. Esta función puede sustituirse a menudo por el operador /.

floor(x)

class calibre.utils.formatter_functions.BuiltinFloor[fuente]

floor(x) – devuelve el mayor entero que es menor o igual a x. Da un error si x no es un número.

fractional_part(x)

class calibre.utils.formatter_functions.BuiltinFractionalPart[fuente]

fractional_part(x) – devuelve el valor tras el punto decimal. Por ejemplo, fractional_part(3.14) devuelve 0.14. Da un error si x no es un número.

mod(x)

class calibre.utils.formatter_functions.BuiltinMod[fuente]

mod(x) – devuelve floor(resto de x / y). Da un error si x o y no son números.

multiply(x [, y]*)

class calibre.utils.formatter_functions.BuiltinMultiply[fuente]

multiply(x [, y]*) – devuelve el producto de sus argumentos. Da un error si algún argumento no es un número. Esta función puede sustituirse a menudo por el operador *.

round(x)

class calibre.utils.formatter_functions.BuiltinRound[fuente]

round(x) – devuelve el entero más cercano a x. Da un error si x no es un número.

subtract(x, y)

class calibre.utils.formatter_functions.BuiltinSubtract[fuente]

subtract(x, y) – devuelve x - y. Da un error si «x» o «y» no son números. Esta función puede sustituirse a menudo por el operador -.

Boolean

and(valor [, valor]*)

class calibre.utils.formatter_functions.BuiltinAnd[fuente]

and(valor [, valor]*) – devuelve el texto «1» si ninguno de los valores está vacío, en caso contrario, devuelve un texto vacío. Esta función funciona bien con «first_non_empty». Puede incluir tantos valores como quiera. En muchas situaciones el operador && puede sustituir a esta función.

not(valor)

class calibre.utils.formatter_functions.BuiltinNot[fuente]

not(valor) – devuelve el texto «1» si el valor está vacío, en caso contrario, devuelve un texto vacío. Esta función funciona bien con «test» o «first_non_empty». En muchas situaciones el operador ! puede sustituir a esta función.

or(valor [, valor]*)

class calibre.utils.formatter_functions.BuiltinOr[fuente]

or(valor [, valor]*) – devuelve el texto «1» si alguno de los valores no está vacío, en caso contrario, devuelve un texto vacío. Esta función funciona bien con «first_non_empty». Puede incluir tantos valores como quiera. En muchas situaciones el operador || puede sustituir a esta función.

Date functions

date_arithmetic(fecha, cálculo, fmt)

class calibre.utils.formatter_functions.BuiltinDateArithmetic[fuente]

date_arithmetic(fecha, cálculo, fmt) – Calcula una nueva fecha a partir de «fecha» usando «cálculo». Devuelve la nueva fecha con el formato opcional «fmt»: si so se proporciona, el resultado estará en formato ISO. El argumento «cálculo» es un texto formado por pares «vQ» («valorQué») donde «v» es un número que puede ser negativo y «Q» es una de las letras siguientes: «s»: añade «v» segundos a «fecha», «m»: añade «v» minutos a «fecha», «h»: añade «v» horas a «fecha», «d»: añade «v» días a «fecha», «w»: añade «v» semanas a «fecha», «y»: añade «v» años a «fecha», donde un año son 365 días. Ejemplo: “1s3d-1m” añade 1 segundo, añade 3 días y resta 1 minuto a «fecha».

days_between(fecha1, fecha2)

class calibre.utils.formatter_functions.BuiltinDaysBetween[fuente]

days_between(fecha1, fecha2) – devuelve el número de días entre «fecha1» y «fecha2». El número es positivo si «fecha1» es posterior a «fecha2», en caso contrario es negativo. Si «fecha1» o «fecha2» no son fechas, la función devuelve un texto vacío.

today()

class calibre.utils.formatter_functions.BuiltinToday[fuente]

today() – devuelve un texto para la fecha de hoy. Este valor está preparado para usarse con «format_date» o «days_between», pero puede manipularse como cualquier otro texto. La fecha está en formato ISO.

Formatting values

finish_formatting(val, fmt, prefijo, sufijo)

class calibre.utils.formatter_functions.BuiltinFinishFormatting[fuente]

finish_formatting(val, fmt, prefijo, sufijo) – aplica el formato, prefijo y sufijo a un valor de la misma manera que se haría en una plantilla como «{series_index:05.2f| - |- }». Por ejemplo, el siguiente programa produce la misma salida que la plantilla anterior: program: finish_formatting(field(«series_index»), «05.2f», « - «, « - «)

format_date(val, texto_formato)

class calibre.utils.formatter_functions.BuiltinFormatDate[fuente]

format_date(val, texto_formato) – da formato a un valor, que debe ser un campo de fecha, según «texto_formato» y devuelve un texto. Los códigos de formato son: «d»: el día como un número, sin cero inicial (de 1 a 31). «dd»: el día como un número, con un cero inicial si es necesario (de 01 a 31). «ddd»: el día de la semana abreviado según el idioma local (p. ej. de «lun» a «dom»). «dddd»: el nombre local completo del día de la semana (p. ej. de «lunes» a «domingo»). «M»: el mes como un número sin cero inicial (de 1 a 12). «MM»: el mes como un número, con un cero inicial si es necesario (de 01 a 12). «MMM»: el mes abreviado según el idioma local (p. ej. de «ene» a «dic»). «MMMM»: el nombre local completo del mes (p. ej. de «enero» a «diciembre»). «yy»: el año como un número de dos cifras (de 00 a 99). «yyyy»: el año como un número de cuatro cifras. «h»: la hora sin cero inicial (de 0 a 11 o de 0 a 23, según am/pm). «hh»: la hora con un cero inicial si es necesario (de 00 a 11 o de 00 a 23, según am/pm). «m»: los minutos sin cero inicial (de 0 a 59). «mm»: los minutos con un cero inicial si es necesaro (de 00 a 59). «s»: los segundos sin cero inicial (de 0 a 59). «ss»: los segundos con un cero inicial si es necesario (de 00 a 59). «ap»: se escribe la hora en formato de 12 horas en lugar de 24, con «ap» sustituido por la forma local para am o pm. «AP»: se escribe la hora en formato de 12 horas en lugar de 24, con «AP» sustituido por la forma local para AM o PM. «iso»: la fecha con hora y zona horaria. Debe ser el único formato presente. «to_number»: la fecha como un número decimal. «from_number[:fmt]»: se da formato al código de tiempo según «fmt» si está presente, si no se usa «iso»

format_date_field(nombre_campo, texto_formato)

class calibre.utils.formatter_functions.BuiltinFormatDateField[fuente]

format_date_field(nombre_campo, texto_formato) – da formato al valor en el campo «nombre_campo», que debe ser el nombre de búsqueda de un campo de fecha, ya sea estándar o personalizado. Ver «format_date» para los códigos de formato. Esta función es mucho más rápida que «format_date» y debe usarse cuando se esté dando formato al valor en un campo (columna). No puede usarse con fechas calculadas o variables de texto. Ejemplo: format_date_field(“pubdate”, “yyyy.MM.dd”)

format_number(v, plantilla)

class calibre.utils.formatter_functions.BuiltinFormatNumber[fuente]

format_number(v, plantilla) – da formato al número «v» usando una plantilla de formato de Python como «{0:5.2f}» o «{0:,d}» o «{0:5,.2f}». La parte del nombre de la plantilla debe ser un 0 (cero) (el «{0:» en los ejemplos anteriores). Véase la documentación del lenguaje de plantillas y de Python para más ejemplos. Si la plantilla contiene sólo un formato, puede omitir el «{0:» inicial y el «}» final. Devuelve un texto vacío si el formato falla.

human_readabe(v)

class calibre.utils.formatter_functions.BuiltinHumanReadable[fuente]

human_readabe(v) – devuelve un texto que representa el número «v» en KB, MB, GB, etc.

rating_to_stars(valor, usar_media_estrella)

class calibre.utils.formatter_functions.BuiltinRatingToStars[fuente]

rating_to_stars(valor, usar_media_estrella) – Devuelve la calificación como caracteres de estrella. El valor es un número entre 0 y 5. Ponga usar_media_estrella a 1 si quiere caracteres de media estrella para columnas personalizadas de calificación que permiten números no enteros, por ejemplo 2.5.

urls_from_identifiers(identificadores, ordenar_resultado)

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[fuente]

urls_from_identifiers(identificadores, ordenar_resultado) – dada una lista de identificadores separados por comas, donde cada identificador es un par de valores separados por dos puntos (nombre_id:valor_id), devuelve una lista de URL HTML separados por comas, generados a partir de los identificadores. La lista no está ordenada si ordenar_resultado es 0 (carácter o número), en caso contrario se ordena alfabéticamente según el nombre del identificador. Los URL se generan de la misma manera que la columna predefinida de identificadores cuando se muestra en los detalles del libro.

Get values from metadata

annotation_count()

class calibre.utils.formatter_functions.BuiltinAnnotationCount[fuente]

annotation_count() – devuelve el número total de anotaciones de todos los tipos en el libro actual. Esta función sólo funciona en la interfaz gráfica.

approximate_formats()

class calibre.utils.formatter_functions.BuiltinApproximateFormats[fuente]

approximate_formats() – devuelve una lista separada por comas de formatos que en algún momento estuvieron asociados con el libro. No hay garantía de que esta lista sea correcta, aunque probablemente lo sea. Esta función puede ejecutarse en el modo de programación de plantillas usando la plantilla «{:”approximate_formats()”}». Tenga en cuenta que los nombres de formato están siempre en mayúsculas, como en «EPUB». Esta función funciona sólo en la interfaz gráfica. Si quiere usar estos valores en las plantillas para guardar en el disco o enviar al dispositivo, debe crear una columna personalizada de tipo «Columna generada a partir de otras columnas», usar la función en la plantilla de dicha columna y usar el valor de la columna en las plantillas para guardar o enviar

author_links(sep_val, sep_par)

class calibre.utils.formatter_functions.BuiltinAuthorLinks[fuente]

author_links(sep_val, sep_par) – devuelve un texto que contiene una lista de autores y enlaces a los autores de la forma: «autor1 sep_val enlace_autor1 sep_par autor2 sep_val enlace_autor2» etc. Cada autor está separado de su correspondiente enlace por el texto «sep_val», sin espacios adicionales. Los pares autor:enlace_autor están separados por el texto «sep_par», sin espacios adicionales. Es responsabilidad del usuario proporcionar separadores que no aparezcan en los nombres o enlaces de autor. El nombre de autor se incluye aunque su enlace esté vacío.

author_sorts(separador)

class calibre.utils.formatter_functions.BuiltinAuthorSorts[fuente]

author_sorts(separador) – devuelve un texto que contiene la lista de valores de orden de autor para los autores del libro. El orden de autor es el que figura en los metadatos de autores (diferente del valor de orden de autor del libro). La lista devuelta es de la forma orden_de_autor_1 separador orden_de_autor_2 etc. Los valores de orden de autor en la lista están en el mismo orden que los autores del libro. Si quiere espacios alrededor de «separador», inclúyalos en el valor

booksize()

class calibre.utils.formatter_functions.BuiltinBooksize[fuente]

booksize() – devuelve el valor del campo de tamaño. Esta función funciona sólo en la interfaz gráfica. Si quiere usar estos valores en las plantillas para guardar en el disco o enviar al dispositivo, debe crear una columna personalizada de tipo «Columna generada a partir de otras columnas», usar la función en la plantilla de dicha columna y usar el valor de la columna en las plantillas para guardar o enviar

connected_device_name(ubicación_de_almacenamiento)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[fuente]

connected_device_name(ubicación_de_almacenamiento) – si hay un dispositivo conectado, devuelve el nombre del dispositivo, en caso contrario devuelve un texto vacío. Cada ubicación de almacenamiento de un dispositivo puede tener un nombre diferente. Los nombres de ubicación son «main», «carda» y «cardb». Esta función sólo funciona en la interfaz gráfica.

connected_device_uuid(ubicación_de_almacenamiento)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[fuente]

connected_device_uuid(ubicación_de_almacenamiento) – si un dispositivo está conectado, entonces devuelve el uuid (identificador único) del dispositivo, de lo contrario devuelve un texto vacío. Cada ubicación de almacenamiento de un dispositivo tiene un uuid diferente. Los nombres de las ubicaciones son «main», «carda» y «cardb». Esta función sólo funciona en la interfaz gráfica.

current_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[fuente]

current_library_name() – devuelve la última parte de la ruta a la biblioteca de calibre actual. Esta función puede llamarse en el modo de programación de plantilla usando la plantilla «{:”current_library_name()”}».

current_library_path()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[fuente]

current_library_path() – devuelve la ruta a la biblioteca de calibre actual. Esta función puede usarse en el modo de programación de plantillas utilizando la plantilla «{:”current_library_path()”}».

current_virtual_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[fuente]

current_virtual_library_name() – devuelve el nombre de la biblioteca virtual actual si hay alguna, en caso contrario devuelve un texto vacío. Se conservan mayúsculas y minúsculas. Ejemplo: «program: current_virtual_library_name()». Esta función sólo funciona en la interfaz gráfica.

field(nombre_de_búsqueda)

class calibre.utils.formatter_functions.BuiltinField[fuente]

field(nombre_de_búsqueda) – Devuelve el campo de metadatos identificado por «nombre_de_búsqueda»

formats_modtimes(formato_fecha)

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[fuente]

formats_modtimes(formato_fecha) – devuelve una lista de elementos (separados por dos puntos) separados por comas que representa las fechas de modificación para los formatos de un libro. El argumento «formato_fecha» especifica cómo se da formato a la fecha. Véase la función «format_date» para más detalles. Puede usar la función «select» para obtener la fecha de modificación de un formato específico. Tenga en cuenta que los nombres de formato siempre están en mayúsculas, como en «EPUB».

formats_paths()

class calibre.utils.formatter_functions.BuiltinFormatsPaths[fuente]

formats_paths() – devuelve una lista separada por comas de elementos separados por dos puntos que representan la ruta completa a los formatos de un libro. Puede usar la función «select» para obtener la ruta a un formato específico. Tenga en cuenta que los nombres de formato están siempre en mayúsculas, como en «EPUB».

formats_sizes()

class calibre.utils.formatter_functions.BuiltinFormatsSizes[fuente]

formats_sizes() – devuelve una lista de elementos (separados por dos puntos) separados por comas que representa los tamaños en bytes de los formatos de un libro. Puede usar la función «select» para obtener el tamaño de un formato específico. Tenga en cuenta que los nombres de formato siempre están en mayúsculas, como en «EPUB».

has_cover()

class calibre.utils.formatter_functions.BuiltinHasCover[fuente]

has_cover() – devuelve «Yes» si el libro tiene portada, en caso contrario devuelve un texto vacío

is_marked()

class calibre.utils.formatter_functions.BuiltinIsMarked[fuente]

is_marked() – comprueba si el libro está «marcado» en calibre, Si lo está, devuelve el valor de la marca, bien «true» (en minúsculas) o bien la lista separada por comas de los nombres de marcan. Devuelve «» (un texto vacío) si el libro no está marcado.

language_codes(nombres)

class calibre.utils.formatter_functions.BuiltinLanguageCodes[fuente]

language_codes(nombres) – devuelve los códigos de idioma correspondientes a «nombres». Los nombres deben estar en el idioma actual. El argumento «nombres» es una lista separada por comas.

language_strings(códigos, localizar)

class calibre.utils.formatter_functions.BuiltinLanguageStrings[fuente]

language_strings(códigos, localizar) – devuelve los nombres de los idiomas identificados por «códigos». Si «localizar» es cero, devuelve los nombres en inglés. Si «localizar» no es cero, devuelve los nombres en el idioma actual. El argumento «códigos» es una lista separada por comas.

ondevice()

class calibre.utils.formatter_functions.BuiltinOndevice[fuente]

ondevice() – devuelve «Yes» si ondevice está activado, si no, devuelve un texto vacío. Esta función funciona sólo en la interfaz gráfica. Si quiere usar estos valores en las plantillas para guardar en el disco o enviar al dispositivo, debe crear una columna personalizada de tipo «Columna generada a partir de otras columnas», usar la función en la plantilla de dicha columna y usar el valor de la columna en las plantillas para guardar o enviar

raw_field(nombre_de_búsqueda [, predeterminado_opcional])

class calibre.utils.formatter_functions.BuiltinRawField[fuente]

raw_field(nombre_de_búsqueda [, predeterminado_opcional]) – devuelve el campo de metadatos nombrado por «nombre_de_búsqueda» sin aplicar ningún formato. Evalúa y devuelve el segundo argumento opcional «predeterminado_opcional» si el campo no está definido («None»).

raw_list(nombre_de_búsqueda, separador)

class calibre.utils.formatter_functions.BuiltinRawList[fuente]

raw_list(nombre_de_búsqueda, separador) – devuelve la lista de metadatos nombrada por «nombre_de_búsqueda» sin aplicar ningún formato u ordenación, con los elementos separados por «separador».

series_sort()

class calibre.utils.formatter_functions.BuiltinSeriesSort[fuente]

series_sort() – devuelve el valor del orden de serie

user_categories()

class calibre.utils.formatter_functions.BuiltinUserCategories[fuente]

user_categories() – devuelve una lista separada por comas de las categorías de usuario que contiene este libro. Esta función sólo funciona en la interfaz gráfica. Si desea utilizar estos valores en las plantillas para guardar en el disco o enviar al dispositivo, deberá crear una plantilla personalizada «Columna generada a partir de otras columnas», utilice la función en la plantilla de esa columna y use el valor de esa columna en las plantillas para guardar o enviar

virtual_libraries()

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[fuente]

virtual_libraries() – devuelve una lista separada por comas de bibliotecas virtuales que contienen este libro. Esta función funciona sólo en la interfaz gráfica. Si quiere usar estos valores en las plantillas para guardar en el disco o enviar al dispositivo, debe crear una columna personalizada de tipo «Columna generada a partir de otras columnas», usar la función en la plantilla de dicha columna y usar el valor de la columna en las plantillas para guardar o enviar

If-then-else

check_yes_no(nombre_de_campo, no_definido, es_false, es_true)

class calibre.utils.formatter_functions.BuiltinCheckYesNo[fuente]

check_yes_no(nombre_de_campo, no_definido, es_false, es_true) – comprueba si el valor del campo de tipo sí o no identificado por la clave de búsqueda nombre_de campo coincide con el valor especificado en los parámetros. Devuelve «yes» si coincide, y un texto vació en caso contrario. Establezca el parámetro no_definido, es_false o es_truea 1 (el número) para comprobar dicha condición, en caso contrario establézcalos a 0. Ejemplo: check_yes_no(«#bool», 1, 0, 1) devuelve «yes» si el campo de tipo sí o no «#bool» es True o no está definido (no es ni True ni False). Más de uno de no_definido, es_false y es_true puede ser 1. Esta función se usa normalmente con las funciones test() o is_empty().

contains(val, patrón, texto_si_coincide, texto_si_no_coincide)

class calibre.utils.formatter_functions.BuiltinContains[fuente]

contains(val, patrón, texto_si_coincide, texto_si_no_coincide) – comprueba si «val» contiene coincidencias para la expresión regular «patrón». Devuelve «texto_si_coincide» si se encuentran coincidencias, en caso contrario devuelve «texto_si_no_coincide»

field_exists(nombre_campo)

class calibre.utils.formatter_functions.BuiltinFieldExists[fuente]

field_exists(nombre_campo) – comprueba si existe un campo (columna) llamado nombre_campo, devuelve “1” si es así y “” si no.

ifempty(val, texto_si_vacío)

class calibre.utils.formatter_functions.BuiltinIfempty[fuente]

ifempty(val, texto_si_vacío) – devuelve «val» si no está vacío, en caso contrario devuelve «texto_si_vacío»

test(val, texto_si_no_vacío, texto_si_vacío)

class calibre.utils.formatter_functions.BuiltinTest[fuente]

test(val, texto_si_no_vacío, texto_si_vacío) – devuelve «texto_si_no_vacío» si «val» no está vacío, «devuelve texto_si_vacío» en caso contrario

Iterating over values

first_non_empty(valor [, valor]*)

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[fuente]

first_non_empty(valor [, valor]*) – devuelve el primer valor que no esté vacío. Si todos los valores están vacíos, se devuelve también un texto vacío. Puede incluir tantos valores como quiera.

lookup(val, [patrón, campo,]+ otro_campo)

class calibre.utils.formatter_functions.BuiltinLookup[fuente]

lookup(val, [patrón, campo,]+ otro_campo) – como «switch», pero los argumentos son nombres de campo (metadatos), no texto. Se usará el valor del campo apropiado. Tenga en cuenta que puesto que las columnas compuestas son campos, puede usar esta función en un campo compuesto para usar el valor de otro campo compuesto. Esto es muy útil para construir rutas de guardado variables

switch(val, [patrón, valor,]+ otro_valor)

class calibre.utils.formatter_functions.BuiltinSwitch[fuente]

switch(val, [patrón, valor,]+ otro_valor) – para cada pareja «patrón, valor», comprueba si «val» contiene coincidencias para la expresión regular «patrón» y, en tal caso, devuelve ese valor. Si no coincide ningún patrón, devuelve «otro_valor». Puede emplear tantas parejas «patrón, valor» como desee

switch_if([expresión_de prueba, expresión_de_valor,]+ otra_expresión)

class calibre.utils.formatter_functions.BuiltinSwitchIf[fuente]

switch_if([expresión_de prueba, expresión_de_valor,]+ otra_expresión) – para cada par «expresión_de_prueba, expresión_de_valor», comprueba si expresión_de_prueba es True (no vacía) y, de ser así, devuelve el resultado de expresión_de_valor. Si ninguna expresión_de_prueba es True, se devuelve el resultado de otra_expresión. Puede haber tantos pares «expresión_de_prueba, expresión_de_valor» como se desee.

List lookup

identifier_in_list(val, nombre_id [, val_encontrado, val_no_encontrado])

class calibre.utils.formatter_functions.BuiltinIdentifierInList[fuente]

identifier_in_list(val, nombre_id [, val_encontrado, val_no_encontrado]) – trata val como una lista de identificadores separados por comas. Un identificador tiene el formato nombre_id:valor. El parámetro nombre_id es el texto de nombre de identificador a buscar, ya sea nombre_id o nombre_id:expreg. El primer caso coincide si hay algún identificador con ese nombre. El segundo caso coincide si algún identificador tiene ese nombre y la expresión regular coincide con el valor del identificador. Si se proporcionan val_encontrado y val_no_encontrado, si hay una coincidencia, se devuelve val_encontrado; de lo contrario, se devuelve val_no_encontrado. Si val_encontrado y val_no_encontrado están ausentes, si hay alguna coincidencia, se devuelve el par identificador:valor; de lo contrario, un texto vacío.

in_list(val, separador, [patrón, encontrado,]+ no_encontrado)

class calibre.utils.formatter_functions.BuiltinInList[fuente]

in_list(val, separador, [patrón, encontrado,]+ no_encontrado) – considerando «val» como una lista de elementos separados por «separador», si «patrón» coincide con alguno de los elementos de la lista, devuelve «encontrado». Si «patrón» no coincide con ningún elemento, devuelve «no_encontrado». El par de argumentos «patrón» y «encontrado» puede repetirse tantas veces como se desee. Los patrones se evalúan en orden. Se devuelve «encontrado» para la primera coincidencia. Sinónimos: in_list(), list_contains()

list_item(val, índice, separador)

class calibre.utils.formatter_functions.BuiltinListitem[fuente]

list_item(val, índice, separador) – interpreta el campo como una lista de elementos separados por «separador» y devuelve el elemento número «índice». El primer elemento es el número cero. El último elemento puede obtenerse con «list_item(-1, separador)». Si el elemento no está en la lista devuelve un valor vacío. El separador tiene el mismo significado que en la función «count».

select(val, clave)

class calibre.utils.formatter_functions.BuiltinSelect[fuente]

select(val, clave) – interpreta el valor como una lista de elementos separados por comas, con los elementos de la forma «id:valor». Encuentra la pareja con «id» igual a «clave» y devuelve el valor correspondiente. Devuelve un texto vacío si no se encuentra ninguna coincidencia.

str_in_list(val, separador, [texto, encontrado,]+ no_encontrado)

class calibre.utils.formatter_functions.BuiltinStrInList[fuente]

str_in_list(val, separador, [texto, encontrado,]+ no_encontrado) – considerando «val» como una lista de elementos separados por «separador», si «texto» coincide con alguno de los elementos de la lista, devuelve «encontrado». Si «texto» no coincide con ningún elemento, devuelve «no_encontrado». La camparación es de tipo exacto (no por subtexto) y no distingue mayúsculas y minúsculas. El par de argumentos «texto» y «encontrado» puede repetirse tantas veces como se desee. Los patrones se evalúan en orden. Se devuelve «encontrado» para la primera coincidencia.

List manipulation

count(val, separador)

class calibre.utils.formatter_functions.BuiltinCount[fuente]

count(val, separador) – interpreta el valor como una lista de elementos separados por «separador» y devuelve el número de elementos de la lista. La mayoría de las listas usan una coma como separador, pero «authors» usa un ampersand («&»). Ejemplos: {tags:count(,)}`, `{authors,count(&)}. Sinónimos: count(), list_count()

list_count_matching(lista, patrón, separador)

class calibre.utils.formatter_functions.BuiltinListCountMatching[fuente]

list_count_matching(lista, patrón, separador) – interpreta «lista» como una lista de elementos separados por «separador» y devuelve el número de elementos de la lista que coinciden con la expresión regular «patrón»». Sinónimos: list-count_matching(), count_matching()

list_difference(lista1, lista2, separador)

class calibre.utils.formatter_functions.BuiltinListDifference[fuente]

list_difference(lista1, lista2, separador) – devuelve una lista construida eliminando de lista1 cualquier elemento que aparezca en lista2, sin distinguir mayúsculas y minúsculas. Los elementos de lista1 y lista2 están separados por separador, así como los de la lista resultante.

list_equals(lista1, sep1, lista2, sep2, val_sí, val_no)

class calibre.utils.formatter_functions.BuiltinListEquals[fuente]

list_equals(lista1, sep1, lista2, sep2, val_sí, val_no) – devuelve «val_sí» si «lista1» y «lista2» contienen los mismos elementos, en caso contrario devuelve «val_no». Los elementos se determinan dividiendo cada lista por el correspondiente carácter separador («sep1» o «sep2»). El orden de los elementos no es relevante. La comparación no diferencia mayúsculas y minúsculas.

list_intersection(lista1, lista2, separador)

class calibre.utils.formatter_functions.BuiltinListIntersection[fuente]

list_intersection(lista1, lista2, separador) – devuelve una lista construida eliminando de lista1 cualquier elemento que no aparezca en lista2, sin distinguir mayúsculas y minúsculas. Los elementos de lista1 y lista2 están separados por separador, así como los de la lista resultante.

list_join(con_separador, lista1, separador1 [, lista2, separador2]*)

class calibre.utils.formatter_functions.BuiltinListJoin[fuente]

list_join(con_separador, lista1, separador1 [, lista2, separador2]*) – devuelve una lista formada por unión de los elementos en las listas de origen (lista1, etc.) usando con_separador entre los elementos de la lista resultante. Los elementos de cada lista de origen lista[123…] están separador por el correspondiente separador[123…]. Una lista puede contener cero elementos. Puede ser un campo como editorial que contiene un solo valor, una lista de un elemento en la práctica. Los duplicados se eliminan sin distinción de mayúsculas y minúsculas. Los elementos se devuelven en el orden en que aparecen en las listas de origen. Si los elementos difieren sólo en mayúsculas y minúsculas, se usa la última aparición. Todos los separadores pueden ser de más de un carácter. Ejemplo:

program:

list_join(“#@#”, $authors, “&”, $tags, “,”)

Puede usar list_join sobre los resultados de ejecuciones previas de list_join de la siguiente manera:

program:

a = list_join(“#@#”, $authors, “&”, $tags, “,”); b = list_join(“#@#”, a, “#@#”, $#genre, “,”, $#people, “&”)

Puede usar expresiones para generar una lista. Por ejemplo, supongamos que quiere los elementos de authors y #genre, pero con estos últimos reemplazados por la palabra «Genre: » seguida de la primera letra del elemento, es decir con «Ficción» convertido en «Genre: F». Esto se consigue con lo siguiente:

program:

list_join(“#@#”, $authors, “&”, list_re($#genre, “,”, “^(.).*$”, “Genre: 1”), “,”)

list_re(lista_orig, separador, incluir, sust_opc)

class calibre.utils.formatter_functions.BuiltinListRe[fuente]

list_re(lista_orig, separador, incluir, sust_opc) – construye una lista separando primero «lista_orig» en elementos usando el carácter «separador». Para cada elemento en la lista, comprueba si coincide con la expresión regular «incluir». Si coincide, se añade a la lista final. Si «sust_opc» no es un texto vacío, se aplica la sustitución antes de añadir el elemento a la lista final.

list_re_group(list_orig, separador, incluir, búsqueda [, plantilla_grupo]+)

class calibre.utils.formatter_functions.BuiltinListReGroup[fuente]

list_re_group(list_orig, separador, incluir, búsqueda [, plantilla_grupo]+) – como «list_re», pero las sustituciones no son opcionales. Usa re_group(elemento, búsqueda, plantilla_grupo, …) al hacer sustituciones en la lista final.

list_remove_duplicates(lista, separador)

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[fuente]

list_remove_duplicates(lista, separador) – devuelve una lista construida por eliminación de los duplicados en «lista». No distingue mayúsculas y minúsculas, se mantiene la última versión. Los elementos de «lista» están separados por «separador», así como los de la lista resultante.

list_sort(lista, dirección, separador)

class calibre.utils.formatter_functions.BuiltinListSort[fuente]

list_sort(lista, dirección, separador) – devuelve «lista» ordenada sin distinción de mayúsculas y minúsculas. Si «dirección» es cero, la lista se ordena de manera ascendente, en caso contrario, de manera descendente. Los elementos de «lista» están separados por «separador», así como los de la lista resultante.

list_split(lista, sep, prefijo_id)

class calibre.utils.formatter_functions.BuiltinListSplit[fuente]

list_split(lista, sep, prefijo_id) – divide la lista en valores separados por «sep» y asigna los valores a variables con nombre «prefijo_id_N», donde N es la posición del valor en la lista. El primer elemento tiene posición 0 (cero). La función devuelve el último elemento de la lista. Ejemplo: split(“uno:dos:bla”, “:”, “var”) es equivalente a var_0 = “uno”; var_1 = “dos”; var_2 = “bla”.

list_union(lista1, lista2, separador)

class calibre.utils.formatter_functions.BuiltinListUnion[fuente]

list_union(lista1, lista2, separador) – devuelve una lista construida por combinación de los elementos en «lista1» y «lista2», eliminando los duplicados de «lista2» (no distingue mayúsculas y minúsculas, se mantiene la versión de «lista1»). Los elementos de «lista1» y «lista2» están separados por «separador», así como los de la lista resultante. Sinónimos: list_union(), merge_list()

range(inicio, fin, paso, límite)

class calibre.utils.formatter_functions.BuiltinRange[fuente]

range(inicio, fin, paso, límite) – devuelve una lista de números generados por iteración en el intervalo especificado por los argumentos inicio, fin y paso, con una longitud máxima de límite. El primer elemento generado es inicio. Los valores siguientes son valor_sig = valor_act + paso. El proceso continúa mientras valor_sig < fin, suponiendo que paso sea positivo, en caso contrario mientras valor_sig > fin. Si inicio no cumple la condición, es decir si inicio >= fin (en caso de que paso sea positivo), se devuelve una lista vacía. El límite establece la longitud máxima de la lista y tiene un valor predeterminado de 1000. Los argumentos inicio, paso y límite son opcionales. Si se ejecuta range() con un argumento, se interpreta como fin. Dos argumentos especifican inicio y fin. Tres argumentos especifican inicio, fin y paso. Cuatro argumentos especifican inicio, fin, paso y límite. Ejemplos: range(5) -> “0,1,2,3,4”. range(0,5) -> “0,1,2,3,4”. range(-1,5) -> “-1,0,1,2,3,4”. range(1,5) -> “1,2,3,4”. range(1,5,2) -> “1,3”. range(1,5,2,5) -> “1,3”. range(1,5,2,1) -> error(límite superado).

subitems(val, índice_inicio, índice_fin)

class calibre.utils.formatter_functions.BuiltinSubitems[fuente]

subitems(val, índice_inicio, índice_fin) – Esta función se usa para separar listas de elementos tales como los géneros. Interpreta el valor como una lista de elementos separados por comas, donde cada elemento es a su vez una lista de elementos separados por puntos. Devuelve una nueva lista formada tomando, de cada lista de elementos separados por puntos, los elementos situados entre las posiciones «índice_inicio» e «índice_fin» y combinando los resultados. El primer elemento de cada lista separada por puntos ocupa la posición cero. Si un índice es negativo, se cuenta desde el final de la lista. Como caso especial, si «índice_fin» es cero, se considera que es el final de la lista. Ejemplos en el modo básico de plantilla y suponiendo que la columna #género contiene el valor «A.B.C»: {#género:subitems(0,1)} devuelve «A», {#género:subitems(0,2)} devuelve «A.B», {#género:subitems(1,0)} devuelve «B.C». Suponiendo que #género contiene el valor «A.B.C, D.E.F»: {#género:subitems(0,1)} devuelve «A, D», {#género:subitems(0,2)} devuelve «A.B, D.E».

sublist(val, índice_inicio, índice_fin, separador)

class calibre.utils.formatter_functions.BuiltinSublist[fuente]

sublist(val, índice_inicio, índice_fin, separador) – interpreta el valor como una lista de elementos separados por «separador» y devuelve una nueva lista con los elementos comprendidos entre la posición «índice_inicio» e «índice_fin». El primer elemento ocupa la posición cero. Si un índice es negativo, se cuenta desde el final de la lista. Como caso especial, si «índice_fin» es cero, se considera que es el final de la lista. Ejemplos en el modo básico de plantilla y suponiendo que la columna de etiquetas (definida como valores separados por comas) contiene «A, B, C»: {tags:sublist(0,1,\,)} devuelve «A». {tags:sublist(-1,0,\,)} devuelve «C». {tags:sublist(0,-1,\,)} devuelve «A, B».

Other

arguments(id[=expresión] [, id[=expresión]]*)

class calibre.utils.formatter_functions.BuiltinArguments[fuente]

arguments(id[=expresión] [, id[=expresión]]*) – Usado en una plantilla guardada para obtener el valor de los argumentos pasados en la invocación. Esta función declara e inicializa las variables locales, parámetros en la práctica. Las variables son posicionales: obtienen el valor del argumento usado en la misma posición en la invocación. Si el argumento correspondiente no aparece en la invocación, «arguments» asigna a esa variable el valor predeterminado suministrado. Si no hay valor predeterminado, será un texto vacío.

assign(id, val)

class calibre.utils.formatter_functions.BuiltinAssign[fuente]

assign(id, val) – asigna «val» a «id» y devuelve «val». «id» debe ser un identificador, no una expresión. Esta función puede sustituirse a menudo por el operador =.

globals(id[=expresión] [, id[=expressión]]*)

class calibre.utils.formatter_functions.BuiltinGlobals[fuente]

globals(id[=expresión] [, id[=expressión]]*) – Obtiene «variables globales» que pueden pasarse al formateador. Esta función declara e inicializa variables locales con los nombres de las variables globales que se le pasan. Si la variable correspondiente no está presente en las variables globales creadas, asignará a dicha variable el valor predeterminado suministrado. Si no hay valor predeterminado, se asigna a la variable el texto vacío.

print(a [, b]*)

class calibre.utils.formatter_functions.BuiltinPrint[fuente]

print(a [, b]*) – escribe los argumentos en la salida estándar. Sólo será visible si inicia calibre a partir de línea de órdenes (calibre-debug -g).

Recursion

eval(plantilla)

class calibre.utils.formatter_functions.BuiltinEval[fuente]

eval(plantilla) – evalúa la plantilla, pasando las variables locales (las definidas con «assign») en lugar de los metadatos del libro. Esto permite usar el procesador de plantillas para elaborar resultados complejos a partir de variables locales. Dado que los caracteres «{» y «}» tienen un uso especial, debe usar «[[» en lugar de «{» y «]]» para «}»; se convertirán automáticamente. Tenga en cuenta que los prefijos y sufijos (la sintaxis «|prefijo|sufijo|») no puede usarse en el argumento de esta función en el modo de programación de plantilla.

Relational

cmp(x, y, mn, ig, my)

class calibre.utils.formatter_functions.BuiltinCmp[fuente]

cmp(x, y, mn, ig, my) – compara x e y después de convertirlas en números. Devuelve mn si x < y. Devuelve ig si x = y. Devuelve my en otros casos. En muchas ocasiones los operadores de comparación numérica (>#, <#, ==#, etc.) pueden sustituir a esta función.

first_matching_cmp(val [, cmp1, resultado1]+, otro_resultado)

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[fuente]

first_matching_cmp(val [, cmp1, resultado1]+, otro_resultado) – compara «val < cmpN» consecutivamente y devuelve «resultadoN» para la primera comparación que sea cierta. Devuelve «otro_resultado» si ninguna comparación es cierta. Ejemplo: first_matching_cmp(10,5,»pequeño»,10,»mediano»,15,»grande»,»enorme») devuelve «grande». El mismo ejemplo con un primer valor de 16 devuelve «enorme».

strcmp(x, y, mn, ig, my)

class calibre.utils.formatter_functions.BuiltinStrcmp[fuente]

strcmp(x, y, mn, ig, my) – compara «x» e «y» como textos sin distinguir mayúsculas y minúsculas. Devuelve «mn» si x < y. Devuelve «ig» si x = y. Devuelve «my» en otros casos. En muchas ocasiones los operadores de comparación lexicográfica (>, <, ==, etc.) pueden sustituir a esta función.

strcmpcase(x, y, mn, ig, my)

class calibre.utils.formatter_functions.BuiltinStrcmpcase[fuente]

strcmpcase(x, y, mn, ig, my) – hace una comparación con distinción de mayúsculas y minúsculas entre x e y. Devuelve mn si x < y. Devuelve ig si x = y, o my en caso contrario. Atención: Éste NO es el comportamiento predeterminado usado por calibre, por ejemplo, en los operadores de comparación léxica (==, >, <, etc.). Esta función podría dar resultados inesperados, use preferentemente strcmp() siempre que sea posible.

String case changes

capitalize(val)

class calibre.utils.formatter_functions.BuiltinCapitalize[fuente]

capitalize(val) – devuelve «val» con la primera letra mayúscula

lowercase(val)

class calibre.utils.formatter_functions.BuiltinLowercase[fuente]

lowercase(val) – devuelve «val» en minúsculas

titlecase(val)

class calibre.utils.formatter_functions.BuiltinTitlecase[fuente]

titlecase(val) – devuelve «val» con las iniciales en mayúscula

uppercase(val)

class calibre.utils.formatter_functions.BuiltinUppercase[fuente]

uppercase(val) – devuelve «val» en mayúsculas

String manipulation

character(nombre_carácter)

class calibre.utils.formatter_functions.BuiltinCharacter[fuente]

character(nombre_carácter) – devuelve el carácter con nombre «nombre_carácter». Por ejemplo, «character(“newline”)» devuelve un carácter de salto de línea (”n”). Los nombres de carácter admitidos son “newline” (salto de línea), “return” (retorno de carro), “tab” (tabulación) y “backslash” (barra invertida).

re(val, patrón, sustitución)

class calibre.utils.formatter_functions.BuiltinRe[fuente]

re(val, patrón, sustitución) – devuelve «val» después de aplicar la expresión regular. Todas las veces que aparezca «patrón» se sustituirá por «sustitución». Como en todo calibre, se trata de expresiones regulares compatibles con Python

re_group(val, patrón [, plantilla_para_grupo]*)

class calibre.utils.formatter_functions.BuiltinReGroup[fuente]

re_group(val, patrón [, plantilla_para_grupo]*) – devuelve un texto formado por aplicación de la expresión regular «patrón» al valor «val», sustituyendo cada coincidencia con el texto calculado al sustituir cada grupo por el valor devuelto por la correspondiente plantilla. El valor de correspondencia original del grupo está disponible como «$». En el modo de programación de plantillas, como en las funciones «template» y «eval», use «[[» en lugar de «{» y «]]» en lugar de «}». El siguiente ejemplo en modo de programación de plantillas busca un nombre de serie con más de una palabra y pone la primera palabra en mayúsculas: {series:”re_group($, «(S* )(.*)», «[[$:uppercase()]]», «[[$]]»)”}

shorten(val, car_izq, texto_medio, car_der)

class calibre.utils.formatter_functions.BuiltinShorten[fuente]

shorten(val, car_izq, texto_medio, car_der) – devuelve una versión abreviada de «val», consistente en un número «car_izq» de caracteres del principio del «val», seguidos de «texto_medio», seguido de un número «car_der» de caracteres del fina de «val». «car_izq» y «car_der» deben ser números enteros. Por ejemplo, supongamos que el título del libro es «Novísima recopilación de las leyes de España» y que quiere que quepa en un espacio de 15 caracteres a lo sumo. Si usa {title:shorten(8,-,6)}, el resultado será «Novísima-España». Si la longitud del campo es menor que «car_izq» + «car_der» + la longitud de «texto_medio», se usará el campo intacto. Por ejemplo, el título «La colmena» no se cambiará.

strcat(a [, b]*)

class calibre.utils.formatter_functions.BuiltinStrcat[fuente]

strcat(a [, b]*) – admite un número arbitrario de argumentos. Devuelve un texto consistente en la unión de todos los argumentos consecutivamente

strcat_max(máx, texto1 [, prefijo2, texto2]*)

class calibre.utils.formatter_functions.BuiltinStrcatMax[fuente]

strcat_max(máx, texto1 [, prefijo2, texto2]*) – devuelve un texto formado por concatenación de los argumentos. El valor devuelto es inicialmente «texto1». Se van añadiendo parejas «prefijo, texto» al final del valor mientras la longitud del resultado sea menor que «máx». El resultado es siempre al menos «texto1», aunque su longitud sea mayor que «máx». Puede especificar tantas parejas «prefijo, texto» como desee.

strlen(a)

class calibre.utils.formatter_functions.BuiltinStrlen[fuente]

strlen(a) – Devuelve la longitud del texto pasado como argumento

substr(texto, inicio, fin)

class calibre.utils.formatter_functions.BuiltinSubstr[fuente]

substr(texto, inicio, fin) – devuelve los caracteres entre la posición «inicio» y «fin» de «texto». El primer carácter de «texto» está en la posición cero. Si «fin» es negativo, entonces indica la posición contando desde la derecha. Si «fin» es cero, indica el último carácter. Por ejemplo, «substr(“12345”, 1, 0)» devuelve «2345», y «substr(“12345”, 1, -1)» devuelve «234».

swap_around_articles(valor, separador)

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[fuente]

swap_around_articles(valor, separador) – devuelve el valor con los artículos puestos al final. El valor puede ser una lista, en cuyo caso se procesará cada elemento de la lista. Si el valor es una lista, debe proporcionar el separador de los elementos de la lista. Si no se proporciona ningún separador, la lista se trata como un único valor, no una lista.

swap_around_comma(val)

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[fuente]

swap_around_comma(val) – dado un valor de la forma «B, A», devuelve «A B». Esto es útil para convertir nombres en formato APELLIDO, NOMBRE a NOMBRE APELLIDO. Si no hay ninguna coma, la función devuelve «val» sin cambios

to_hex(val)

class calibre.utils.formatter_functions.BuiltinToHex[fuente]

to_hex(val) – devuelve el texto val codificado en hexadecimal. Esto es útil al construir URL de calibre.

transliterate(a)

class calibre.utils.formatter_functions.BuiltinTransliterate[fuente]

transliterate(a) – Devuelve un texto en el alfabeto latino formado por aproximación del sonido de las palabras en el texto origen. Por ejemplo, si el origen es «Фёдор Миха́йлович Достоевский» la función devuelve «Fiodor Mikhailovich Dostoievskii».

Template database functions

book_count(consulta, usar_bv)

class calibre.utils.formatter_functions.BuiltinBookCount[fuente]

book_count(consulta, usar_bv) – devuelve el número de libros encontrados al buscar «consulta». Si «usar_bv» es 0 (cero), se ignoran las bibliotecas virtuales. Esta función sólo puede usarse en la interfaz gráfica.

book_values(columna, consulta, sep, usar_bv)

class calibre.utils.formatter_functions.BuiltinBookValues[fuente]

book_values(columna, consulta, sep, usar_bv) – devuelve una lista de los valores contenidos en la columna «columna», separados por «sep», en los libros encontrados al buscar «consulta». Si «usar_bv» es 0 (cero), se ignoran las bibliotecas virtuales. Esta función sólo puede usarse en la interfaz gráfica.

extra_file_size(nombre_archivo)

class calibre.utils.formatter_functions.BuiltinExtraFileSize[fuente]

extra_file_size(nombre_archivo) – devuelve el tamaño en bytes del archivo adicional «nombre_archivo» en la carpeta «data/» del libro si existe, o -1 en caso contrario. Esta función sólo puede usarse en la interfaz gráfica.

get_link(nombre_campo, valor_campo)

class calibre.utils.formatter_functions.BuiltinGetLink[fuente]

get_link(nombre_campo, valor_campo) – obtiene el enlace para el campo «nombre_campo» con el valor «valor_campo». Si no hay ningún enlace asociado, devuelve “”. Ejemplo: get_link(“tags”, “Ficción”) devuelve el enlace asociado a la etiqueta «Ficción».

get_note(nombre_campo, valor_campo, texto)

class calibre.utils.formatter_functions.BuiltinGetNote[fuente]

get_note(nombre_campo, valor_campo, texto) – obtiene la nota para el campo «nombre_campo» con el valor «valor_campo». Si «texto» está vacío, devuelve el HTML de la nota. Si «texto» no está vacío, devuelve el texto plano de la nota. Si la nota no existe, devuelve “” en ambos casos. Ejemplo: get_note(“tags”, “Ficción”, “”) devuelve el HTML de la nota asociada a la etiqueta «Ficción».

has_note(nombre_campo, valor_campo)

class calibre.utils.formatter_functions.BuiltinHasNote[fuente]

has_note(nombre_campo, valor_campo) – devuelve “1” si el valor «valor campo» en el campo «nombre_campo» tiene una nota asociada, y “” en caso contrario. Ejemplo: has_note(“tags”, “Ficción”) devuelve “1” si la etiqueta «Ficción» tiene una nota asociada y “” en caso contrario.

other

set_globals(id[=expresión] [, id[=expresión]]*)

class calibre.utils.formatter_functions.BuiltinSetGlobals[fuente]

set_globals(id[=expresión] [, id[=expresión]]*) – Establece las «variables globales» que se pueden pasar al formateador. Las variables globales toman el nombre del «id» indicado. Se usa el valor del «id» a no se ser que se suministre una expresión.

API of the Metadata objects

The python implementation of the template functions is passed in a Metadata object. Knowing it’s API is useful if you want to define your own template functions.

class calibre.ebooks.metadata.book.base.Metadata(title, authors=('Desconocido',), other=None, template_cache=None, formatter=None)[fuente]

A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object.

Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: «#mytags».

Use the is_null() method to test if a field is null.

This object also has functions to format fields into strings.

The list of standard metadata fields grows with time is in STANDARD_METADATA_FIELDS.

Please keep the method based API of this class to a minimum. Every method becomes a reserved field name.

is_null(field)[fuente]

Return True if the value of field is null in this object. “null” means it is unknown or evaluates to False. So a title of _(“Unknown”) is null or a language of “und” is null.

Be careful with numeric fields since this will return True for zero as well as None.

Also returns True if the field does not exist.

deepcopy(class_generator=<function Metadata.<lambda>>)[fuente]

Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use deepcopy_metadata() instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.

get_identifiers()[fuente]

Return a copy of the identifiers dictionary. The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don’t want any manipulations of the returned dict to show up in the book.

set_identifiers(identifiers)[fuente]

Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.

set_identifier(typ, val)[fuente]

If val is empty, deletes identifier of type typ

standard_field_keys()[fuente]

return a list of all possible keys, even if this book doesn’t have them

custom_field_keys()[fuente]

return a list of the custom fields in this book

all_field_keys()[fuente]

All field keys known by this instance, even if their value is None

metadata_for_field(key)[fuente]

return metadata describing a standard or custom field.

all_non_none_fields()[fuente]

Return a dictionary containing all non-None metadata fields, including the custom ones.

get_standard_metadata(field, make_copy)[fuente]

return field metadata from the field if it is there. Otherwise return None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

get_all_standard_metadata(make_copy)[fuente]

return a dict containing all the standard field metadata associated with the book.

get_all_user_metadata(make_copy)[fuente]

return a dict containing all the custom field metadata associated with the book.

get_user_metadata(field, make_copy)[fuente]

return field metadata from the object if it is there. Otherwise return None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict.

set_all_user_metadata(metadata)[fuente]

store custom field metadata into the object. Field is the key name not the label

set_user_metadata(field, metadata)[fuente]

store custom field metadata for one column into the object. Field is the key name not the label

remove_stale_user_metadata(other_mi)[fuente]

Remove user metadata keys (custom column keys) if they don’t exist in “other_mi”, which must be a metadata object

template_to_attribute(other, ops)[fuente]

Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense.

smart_update(other, replace_metadata=False)[fuente]

Merge the information in other into self. In case of conflicts, the information in other takes precedence, unless the information in other is NULL.

format_field(key, series_with_index=True)[fuente]

Returns the tuple (display_name, formatted_value)

to_html()[fuente]

A HTML representation of this object.

calibre.ebooks.metadata.book.base.STANDARD_METADATA_FIELDS

The set of standard metadata fields.

'''
All fields must have a NULL value represented as None for simple types,
an empty list/dictionary for complex types and (None, None) for cover_data
'''

SOCIAL_METADATA_FIELDS = frozenset((
    'tags',             # Ordered list
    'rating',           # A floating point number between 0 and 10
    'comments',         # A simple HTML enabled string
    'series',           # A simple string
    'series_index',     # A floating point number
    # Of the form { scheme1:value1, scheme2:value2}
    # For example: {'isbn':'123456789', 'doi':'xxxx', ... }
    'identifiers',
))

'''
The list of names that convert to identifiers when in get and set.
'''

TOP_LEVEL_IDENTIFIERS = frozenset((
    'isbn',
))

PUBLICATION_METADATA_FIELDS = frozenset((
    'title',            # title must never be None. Should be _('Unknown')
    # Pseudo field that can be set, but if not set is auto generated
    # from title and languages
    'title_sort',
    'authors',          # Ordered list. Must never be None, can be [_('Unknown')]
    'author_sort_map',  # Map of sort strings for each author
    # Pseudo field that can be set, but if not set is auto generated
    # from authors and languages
    'author_sort',
    'book_producer',
    'timestamp',        # Dates and times must be timezone aware
    'pubdate',
    'last_modified',
    'rights',
    # So far only known publication type is periodical:calibre
    # If None, means book
    'publication_type',
    'uuid',             # A UUID usually of type 4
    'languages',        # ordered list of languages in this publication
    'publisher',        # Simple string, no special semantics
    # Absolute path to image file encoded in filesystem_encoding
    'cover',
    # Of the form (format, data) where format is, e.g. 'jpeg', 'png', 'gif'...
    'cover_data',
    # Either thumbnail data, or an object with the attribute
    # image_path which is the path to an image file, encoded
    # in filesystem_encoding
    'thumbnail',
))

BOOK_STRUCTURE_FIELDS = frozenset((
    # These are used by code, Null values are None.
    'toc', 'spine', 'guide', 'manifest',
))

USER_METADATA_FIELDS = frozenset((
    # A dict of dicts similar to field_metadata. Each field description dict
    # also contains a value field with the key #value#.
    'user_metadata',
))

DEVICE_METADATA_FIELDS = frozenset((
    'device_collections',   # Ordered list of strings
    'lpath',                # Unicode, / separated
    'size',                 # In bytes
    'mime',                 # Mimetype of the book file being represented
))

CALIBRE_METADATA_FIELDS = frozenset((
    'application_id',   # An application id, currently set to the db_id.
    'db_id',            # the calibre primary key of the item.
    'formats',          # list of formats (extensions) for this book
    # a dict of user category names, where the value is a list of item names
    # from the book that are in that category
    'user_categories',
    # a dict of items to associated hyperlink
    'link_maps',
))

ALL_METADATA_FIELDS =      SOCIAL_METADATA_FIELDS.union(
                           PUBLICATION_METADATA_FIELDS).union(
                           BOOK_STRUCTURE_FIELDS).union(
                           USER_METADATA_FIELDS).union(
                           DEVICE_METADATA_FIELDS).union(
                           CALIBRE_METADATA_FIELDS)

# All fields except custom fields
STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
                           PUBLICATION_METADATA_FIELDS).union(
                           BOOK_STRUCTURE_FIELDS).union(
                           DEVICE_METADATA_FIELDS).union(
                           CALIBRE_METADATA_FIELDS)

# Metadata fields that smart update must do special processing to copy.
SC_FIELDS_NOT_COPIED =     frozenset(('title', 'title_sort', 'authors',
                                      'author_sort', 'author_sort_map',
                                      'cover_data', 'tags', 'languages',
                                      'identifiers'))

# Metadata fields that smart update should copy only if the source is not None
SC_FIELDS_COPY_NOT_NULL =  frozenset(('device_collections', 'lpath', 'size', 'comments', 'thumbnail'))

# Metadata fields that smart update should copy without special handling
SC_COPYABLE_FIELDS =       SOCIAL_METADATA_FIELDS.union(
                           PUBLICATION_METADATA_FIELDS).union(
                           BOOK_STRUCTURE_FIELDS).union(
                           DEVICE_METADATA_FIELDS).union(
                           CALIBRE_METADATA_FIELDS) - \
                           SC_FIELDS_NOT_COPIED.union(
                           SC_FIELDS_COPY_NOT_NULL)

SERIALIZABLE_FIELDS =      SOCIAL_METADATA_FIELDS.union(
                           USER_METADATA_FIELDS).union(
                           PUBLICATION_METADATA_FIELDS).union(
                           CALIBRE_METADATA_FIELDS).union(
                           DEVICE_METADATA_FIELDS) - \
                           frozenset(('device_collections', 'formats',
                               'cover_data'))
# these are rebuilt when needed

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Todo acerca de cómo utilizar expresiones regulares en calibre

Todo acerca de cómo utilizar expresiones regulares en calibre

Las expresiones regulares son funciones utilizadas en muchos lugares en calibre para llevar a cabo sofisticadas manipulaciones de contenido y metadatos de libros electrónicos. Este cursillo es una breve introducción al uso de expresiones regulares en calibre.

Índice

Primero, una advertencia y unas palabras de aliento

¿Dónde puedo utilizar expresiones regulares en calibre?

¿Pero qué es una expresión regular?

¿Me lo puede explicar?

No suena tan mal. ¿Qué viene después?

¡Estupendo! ¡Esto empieza a tener sentido!

Bien, estos caracteres especiales están muy bien, pero ¿qué ocurre si quiero hacer coincidir un punto o un signo de interrogación?

¿Y cuáles son los conjuntos más útiles?

Pero si tengo varios textos diferentes para hacer coincidir, ¿las cosas se complican?

Falta algo…

¿No dijo al principio que hay una manera de hacer que una expresión regular no distinga entre mayúsculas y minúsculas?

Creo que empiezo a entender esto de las expresiones regulares… ¿cómo las utilizo en calibre?

Conversiones

Añadir libros

Modificar metadatos en masa

Referencia rápida

Créditos

Primero, una advertencia y unas palabras de aliento

Esto, inevitablemente, va a ser un poco técnico, después de todo, las expresiones regulares son una herramienta técnica para hacer cosas técnicas. Voy a tener que usar una jerga y conceptos que pueden parecer complicados o enrevesados. Voy a tratar de explicar esos conceptos tan claramente como pueda, pero realmente no puedo hacerlo sin utilizarlos en absoluto. Dicho esto, no se desanime por la jerga según intento explicar las cosas nuevas. Y mientras que las expresiones regulares en sí pueden parecer magia negra y arcana (o, siendo más prosaicos, una serie aleatoria de letras y signos sin sentido), le aseguro que no son tan complicadas. Incluso los que entienden las expresiones regulares muy bien tienen problemas para leer las más complejas, pero escribirlas no es tan difícil: se va construyendo la expresión paso a paso. Dicho esto, dé el primer paso y aventúrese conmigo en lo desconocido.

¿Dónde puedo utilizar expresiones regulares en calibre?

Las expresiones regulares se usan en unos cuantos lugares de calibre. En Buscar y sustituir, en las opciones de conversión, en la detección de metadatos a partir de los nombres de archivo en las configuraciones de importación y en buscar y sustituir al modificar metadatos en masa. El editor de libros de calibre también puede usar expresiones regulares en Buscar y sustituir. Por último, puede usar expresiones regulares al buscar en la lista de libros de calibre y en el visor de libros electrónicos de calibre.

¿Pero qué es una expresión regular?

Una expresión regular es una manera de describir porciones de texto. Una sola expresión regular puede coincidir con una serie de diferentes textos. Esto es lo que hace a las expresiones regulares tan potentes: son una forma concisa de describir un número potencialmente elevado de variaciones.

Nota

Aquí se habla de textos en el sentido en que se usan en los lenguajes de programación: un texto de uno o más caracteres, caracteres que incluyen caracteres reales, números, puntuacion y los llamados espacios en blanco (tabulaciones, saltos de línea, etc.). Tenga en cuenta que en general, los caracteres en mayúsculas y en minúsculas no se consideran iguales, por lo tanto «a» es diferente de «A», etc. En calibre, las expresiones regulares no distinguen mayúsculas y minúsculas en la barra de búsqueda, pero sí en las opciones de conversión. Hay una manera de hacer que las expresiones regulares no distingan nunca entre mayúsculas y minúsculas, pero hablaremos de eso más adelante. La cosa se complica porque las expresiones regulares permiten variaciones en los textos con los que coinciden, de manera que una expresión pueden coincidir con varios textos, que es la causa por la mucha gente no se molesta en usarlas. Más sobre esto en un momento.

¿Me lo puede explicar?

Bueno, para eso estamos aquí. En primer lugar, éste es el concepto más importante en las expresiones regulares: Un texto por sí mismo es una expresión regular que coincide consigo mismo. Es decir, si yo quisiera encontrar el texto "¡Hola a todos!" utilizando una expresión regular, ésta sería ¡Hola a todos!. Y sí, realmente es así de simple. Se dará cuenta, sin embargo, de que esto sólo coincide con el texto exacto "¡Hola a todos!" y no, por ejemplo, con "¡Hola a Todos!" u "¡hola a todos!" o cualquier otra variación.

No suena tan mal. ¿Qué viene después?

Lo que viene después es lo bueno de verdad. ¿Recuerda cuando dije que las expresiones regulares pueden coincidir con varios textos? Aquí se pone un poco más complicado. Digamos, como un ejercicio un poco más práctico, que un libro que quiere convertir tiene un incómodo pie de página con el número de páginas, como «Página 5 de 423». Obviamente, el número de página variará entre 1 y 423, así que deberá buscar 423 textos diferentes, ¿no? Pues no: las expresiones regulares permiten definir conjuntos de caracteres que coinciden: para definir un conjunto, ponga todos los caracteres del conjunto entre corchetes. Así, por ejemplo, el conjunto [abc] coincidirá con los caracteres «a», «b» o «c». Los conjuntos sólo coincidirán con uno de los caracteres del conjunto. Los conjuntos «entienden» gamas de caracteres, es decir, si quisiera hacer coincidir todos los caracteres en minúsculas, tendría que utilizar el conjunto de minúsculas [a-z] y para mayúsculas o minúsculas tendría que utilizar [a-zA-Z], etc. ¿Comprendido? Por lo tanto, usando la expresión Página [0-9] de 423 podrá seleccionar las primeras 9 páginas, reduciendo así las expresiones necesarias a tres. La segunda expresión Página [0-9][0-9] de 423 coincidirá con todos los números de página de dos dígitos, y seguro que puede adivinar cuál sería la tercera expresión. Sí, adelante, escríbala.

¡Estupendo! ¡Esto empieza a tener sentido!

Esperaba que dijera eso. Pero prepárese, ¡ahora se pone aún mejor! Acabamos de ver que con el uso de conjuntos, podemos detectar uno de varios caracteres a la vez. Pero incluso se puede repetir un carácter o conjunto, reduciendo la cantidad de expresiones necesarias para el ejemplo anterior de los números de página a una. Sí, ¡UNA! ¿Entusiasmado? ¡Debería estarlo! Funciona así: Algunos de los llamados caracteres especiales, «+», «?» y «*», repiten el elemento único que los precede. (Elemento significa un único carácter, un conjunto de caracteres, una secuencia de escape o un grupo —aprenderemos más acerca de los dos últimos más adelante—, en resumen, cualquier entidad individual en una expresión regular). Estos caracteres se denominan comodines o cuantificadores. Para ser más precisos, «?» coincide con 0 o 1 copias del elemento anterior, «*» coincide con 0 o más copias del elemento anterior y «+» coincide con 1 o más copias del elemento anterior. Algunos ejemplos: La expresión a? coincidiría con «» (que es un texto vacío, no estrictamente útil en este caso) o «a», la expresión a* coincidiría con «», «a», «aa» o cualquier número de aes seguidas y, finalmente, la expresión a+ coincidiría con «a», «aa» o cualquier número de aes seguidas. (Nota: ¡no coincide con el texto vacío!). Lo mismo ocurre con los conjuntos: La expresión [0-9]+ coincidiría con ¡cualquier número entero! Sé lo que está pensando, y tiene razón: si lo utiliza en el caso anterior de los números de página, ¿no sería la forma de obtener una única expresión que coincida con todos los números de página? ¡Sí, la expresión Página [0-9]+ de 423 coincidiría con cualquier número de página de ese libro!

Nota

Un apunte sobre estos cuantificadores: Generalmente intentan hacer coincidir tanto como sea posible, así que tenga cuidado al usarlos. Esto se conoce como «comportamiento codicioso», seguro que entiende por qué. Se vuelve problemático cuando, por ejemplo, se intenta hacer coincidir una etiqueta. Consideremos, por ejemplo, el texto "<p class="calibre2">Título aquí</p>" y digamos que le gustaría hacer coincidir la etiqueta de apertura (la parte entre el primer par de ángulos, veremos más sobre las etiquetas más adelante). Se podría pensar que la expresión <p.*> se correspondería con esa etiqueta, pero en realidad ¡coincide con todo el texto! (El carácter «.» es otro carácter especial, coincide con cualquier cosa excepto saltos de línea, por lo que, básicamente, la expresión .* coincide con cualquier línea que se pueda imaginar). En lugar de eso, trate de usar <p.*?>, lo que hace al cuantificador «*» no codicioso. Esa expresión sólo coincidiría con la primera etiqueta de apertura, como se pretende. De hecho, hay otra manera de lograr esto. La expresión <p[^>]*> coincidirá con la misma etiqueta de apertura, ya verá por qué después de la siguiente sección. Tenga en cuenta que frecuentemente hay más de una manera de escribir una expresión regular.

Bien, estos caracteres especiales están muy bien, pero ¿qué ocurre si quiero hacer coincidir un punto o un signo de interrogación?

Por supuesto que puede hacerlo: simplemente escriba una barra invertida delante de cualquier carácter especial y se interpretará como un carácter literal, sin ningún significado especial. Esta pareja de barra invertida y un carácter se llama secuencia de escape, y el acto de poner una barra invertida delante de un carácter especial se llama escapar ese carácter. Una secuencia de escape se interpreta como un solo elemento. Hay, por supuesto, secuencias de escape que hacen más que sólo escapar caracteres especiales, por ejemplo "\t" representa una tabulación. Vamos a ver algunas de las secuencias de escape más adelante. Ah, por cierto, en relación con esos caracteres especiales: considere que cualquier carácter que señalemos en esta introducción como poseedor de alguna función es especial y por lo tanto necesita ser escapado si desea que sea un carácter literal.

¿Y cuáles son los conjuntos más útiles?

Sabía que lo preguntaría. Algunos conjuntos útiles son [0-9], que coincide con un solo dígito, [a-z], que coincide con una sola letra minúscula, [A-Z], que coincide con una sola letra mayúscula, [a-zA-Z], que coincide con una sola letra, y [a-zA-Z0-9], que coincide con una sola letra o número. También puede utilizar una secuencia de escape como abreviatura:

\d

equivale a [0-9]

\w

equivale a [a-zA-Z0-9_]

\s

equivale a cualquier espacio en blanco

Nota

«Espacio en blanco» es un término para cualquier cosa que no se imprime. Estos caracteres incluyen el espacio, tabulador, avance de línea, salto de página, retorno de carro, espacios duros, etc.

Nota

Los caracteres de mayúsculas y minúsculas pueden coincidir con mayúsculas y minúsculas si está habilitada la configuración para hacer que las búsquedas no distingan entre mayúsculas y minúsculas. Esta configuración se encuentra, por ejemplo, en Preferencias > Búsqueda en calibre y en el panel de búsqueda en el Visor de libros electrónicos, así como en la herramienta :guliabel:`Modificar libro`.

Como último apunte sobre los conjuntos, también puede definir un conjunto como cualquier carácter excepto los especificados. Esto se hace incluyendo el carácter "^" como el primer carácter en el conjunto. Así, [^a] coincidirá con cualquier carácter excepto «a». Esto se llama complementar el conjunto. Las secuencias de escape anteriores que funcionan como abreviaturas también pueden complementarse. "\D" significa cualquier carácter que no sea un número, siendo por lo tanto equivalente a [^0-9]. Las otras abreviaturas se pueden complementar, como puede imaginar, utilizando la respectiva letra mayúscula en lugar de la minúscula. Volviendo al ejemplo <p[^>]*> de la sección anterior, ahora se puede ver que el conjunto especificado trata de coincidir con cualquier carácter excepto el ángulo de cierre.

Pero si tengo varios textos diferentes para hacer coincidir, ¿las cosas se complican?

No tema, todo sigue siendo bueno, bonito y barato. Considere este ejemplo: El libro que quiere convirtir tiene un «Título» escrito en cada página impar y «Autor» en cada página par. Queda muy bien en la versión impresa, ¿no? Pero en libros electrónicos es molesto. Puede agrupar expresiones enteras en paréntesis normales, y el carácter "|" hará que coincida la expresión a la derecha o la de la izquierda. Combínelas y ya está. ¿Demasiado rápido? Bueno, en primer lugar, agrupamos las expresiones para páginas pares e impares, consiguiendo de este modo (Título)(Autor) como nuestras dos expresiones necesarias. Ahora hacemos las cosas más simples mediante el uso de la barra vertical ("|" se llama barra vertical). Si utiliza la expresión (Título|Autor) hará coincidir «Título» (en las páginas impares) o «Autor» (en las páginas pares). Bueno, ha sido fácil, ¿no?

También puede, por supuesto, usar la barra vertical sin utilizar paréntesis de agrupamiento. ¿Recuerda cuando dije que los cuantificadores repiten el elemento que los precede? Pues bien, la barra vertical funciona de manera un poco diferente. La expresión Título|Autor también coincide con el texto «Título» o el texto «Autor», igual que el ejemplo anterior utilizando agrupamiento. La barra vertical selecciona entre toda la expresión anterior y toda la posterior. Así que, si quiere que coincidan los textos «Calibre» y «calibre» y quiere seleccionar sólo entre la «c» mayúscula y minúscula, tendría que utilizar la expresión (c|C)alibre, donde el agrupamiento asegura que sólo la «C» se selecciona. Si usara c|Calibre, obtendría coincidencias con el texto «c» o el texto «Calibre», que no es lo que quería. En resumen: en caso de duda, utilice el agrupamiento con la barra vertical.

Falta algo…

…espere un momento, hay una última cosa muy interesante que se puede hacer con los grupos. Si tiene un grupo que previamente ha hecho coincidir, puede utilizar referencias a ese grupo posteriormente en la expresión. Los grupos se numeran comenzando por 1, y se referencian escapando el número del grupo, por ejemplo, al quinto grupo se haría referencia como \5. Así, aplicando ([^ ]+) \1 sobre el texto «Prueba Prueba», ¡se haría coincidir todo el texto!

¿No dijo al principio que hay una manera de hacer que una expresión regular no distinga entre mayúsculas y minúsculas?

Sí, lo hice, gracias por prestar atención y recordármelo. Puede decirle a calibre cómo desea que se comporten ciertos aspectos mediante el uso de opciones. Las opciones se incluyen en una expresión mediante la construcción especial (?aquí van las opciones), donde, obviamente, debe sustituir «aquí van las opciones» por las opciones específicas que desee. Para no distinguir mayúsculas y minúsculas, la opción es i, así que incluya (?i) en la expresión. Por lo tanto, (?i)prueba coincidiría con «Prueba», «pRueba», «PRueba» y cualquier variación en las mayúsculas y minúsculas que pueda imaginar.

Otra opción útil hace que el punto coincida con absolutamente cualquier carácter, incluyendo el salto de línea, la opción s. Si desea utilizar varias opciones en una expresión, sólo hay que ponerlas en la misma declaración: (?is) no distingue entre mayúsculas y minúsculas y hace que el punto coincida con todo. No importa qué opción se declara en primer lugar, (?si) sería equivalente a lo anterior.

Creo que empiezo a entender esto de las expresiones regulares… ¿cómo las utilizo en calibre?

Conversiones

Vamos a empezar con las configuraciones de conversión. En la sección Buscar y sustituir, puede introducir una regexp (abreviatura de expresión regular) que describe el texto que se sustituirá durante la conversión. La parte interesante es el asistente. Pulse en la varita mágica y obtendrá una vista previa de lo que calibre «ve» durante el proceso de conversión. Vaya al texto que desea eliminar, selecciónelo y cópielo, péguelo en el campo de expresión regular en la parte superior de la ventana. Si hay partes variables, como números de página o algo así, use conjuntos y cuantificadores para recogerlos, y ya que está en ello, recuerde que debe escapar los caracteres especiales, si hay alguno. Pulse el botón con la etiqueta Probar y calibre resaltará las partes que sustituiría al utilizar la expresión regular. Una vez que esté satisfecho, pulse «Aceptar» y se inicia el proceso de conversión. Tenga cuidado si el origen de la conversión tiene etiquetas como este ejemplo:

Maybe, but the cops feel like you do, Anita. What's one more dead vampire?
New laws don't change that. </p>
<p class="calibre4"> <b class="calibre2">Generated by ABC Amber LIT Conv
<a href="http://www.processtext.com/abclit.html" class="calibre3">erter,
http://www.processtext.com/abclit.html</a></b></p>
<p class="calibre4"> It had only been two years since Addison v. Clark.
The court case gave us a revised version of what life was

(copiado descaradamente de este hilo [https://www.mobileread.com/forums/showthread.php?t=75594"]). Habría que eliminar también algunas de las etiquetas. En este ejemplo, recomendaría empezar con la etiqueta <b class="calibre2">, tiene que acabar con la etiqueta de cierre correspondiente (las etiquetas de apertura son <etiqueta>, las etiquetas de cierre son </etiqueta>), que es simplemente el siguiente </b> en este caso. (Consulte un buen manual de HTML o pregunte en el foro si tiene dudas sobre este punto). La etiqueta de apertura puede describirse usando <b.*?>, la etiqueta de cierre usando </b>, por lo que podríamos quitar todo lo que está entre estas etiquetas usando <b.*?>.*?</b>. Pero usar esta expresión sería una mala idea, ya que elimina todo lo encerrado entre las etiquetas <b> (que, por cierto, hacen que el texto encerrado se muestre en negrita), y es casi seguro que eliminaremos porciones del libro de esta manera. En su lugar, incluya también el principio del texto entre las etiquetas, haciendo la expresión regular <b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b>. La \s con cuantificadores se incluye aquí en lugar de utilizar explícitamente los espacios que aparecen en el texto para recoger cualquier variación del texto que pudiera ocurrir. Si prueba una nueva expresión, recuerde revisar qué eliminará calibre para asegurarse de que no elimina partes que desea conservar. Si sólo examina un caso, es posible que pase por alto un desajuste en otro lugar del texto. También tenga en cuenta que en caso de que accidentalmente elimine más o menos etiquetas de las que realmente deseaba, calibre intenta reparar el código dañado después de hacer la eliminación.

Añadir libros

También puede utilizar expresiones regulares para extraer metadatos de los nombres de archivo. Puede encontrar esta función en la sección «Añadir libros» de la configuración. Hay una característica especial: puede utilizar nombres de campo para los campos de metadatos, por ejemplo (?P<title>), que indicaría que calibre utiliza esta parte del texto como el título del libro. Los nombres de los campos permitidos se enumeran en las ventanas, junto con otro útil campo de prueba. Un ejemplo: digamos que desea importar un gran número de archivos llamados así Textos clásicos: La divina comedia de Dante Alighieri.mobi (obviamente, ya lo tiene en la biblioteca, ya que a todos nos gusta la poesía italiana clásica) o Ciencia ficción épica: La Trilogía de la Fundación de Isaac Asimov.epub. Esto es obviamente un esquema de nombres del que calibre no va a extraer datos significativos: la expresión estándar para la extracción de los metadatos es (?P<title>.+) - (?P<author>[^_]+). Una expresión regular que podría funcionar en este caso sería [a-zA-Z]+: (?P<title>.+) de (?P<author>.+). Tenga en cuenta que, dentro del grupo para el campo de metadatos, es necesario utilizar expresiones para describir lo que el campo realmente recoge. Y también tenga en cuenta que, cuando se utiliza el campo de prueba que calibre ofrece, es necesario agregar la extensión de archivo al nombre de archivo de prueba, de lo contrario no recibirá ninguna correspondencia en absoluto, a pesar de utilizar una expresión correcta.

Modificar metadatos en masa

La última parte es Buscar y sustituir con expresiones regulares en los campos de metadatos. Puede acceder a esta función seleccionando varios libros en la biblioteca y usando la modificación de metadatos en masa. ¡Tenga mucho cuidado al utilizar esta última función, ya que puede hacer cosas muy malas en la biblioteca! Verifique que las expresiones hacen lo que quiere que hagan en los campos de prueba, y sólo marque los libros que realmente quiere cambiar. En el modo de búsqueda de expresiones regulares, puede buscar en un campo, sustituir el texto con algo e incluso escribir el resultado en otro campo. Un ejemplo práctico: Digamos que la biblioteca contiene los libros de la serie Dune de Frank Herbert, nombrados de este modo Dune 1 - Dune, Dune 2 - El mesías de Dune y así sucesivamente. Ahora desea incluir Dune en el campo serie. Puede hacerlo mediante la búsqueda (.*?) \d+ - .* en el campo título y sustituyendo con \1 en el campo serie. ¿Ve lo que he hecho? Esto es una referencia al primer grupo de coincidencia en la expresión regular. Ahora que tiene toda la serie lista, sólo tiene que hacer otra búsqueda .*? - en el campo título y reemplazarlo con "" (un texto vacío), de nuevo en el campo título, y los metadatos estarán todos limpios y ordenados. ¿No es genial? Por cierto, en lugar de sustituir todo el campo, puede también añadir al principio o al final del campo, por lo que, si quisiera el título del libro con la información de la serie delante, puede hacerlo también. Como sin duda se ha dado cuenta, hay una casilla etiquetada Distinguir mayúsculas, para que no tenga que usar la opción correspondiente dentro de la expresión regular en este caso.

Bueno, casi llegamos a la conclusión de la muy breve introducción a las expresiones regulares. Esperemos que le haya mostrado lo suficiente como para, al menos, comenzar y seguir aprendiendo por sí mismo. Un buen punto de partida sería la documentación Python para expresiones regulares [https://docs.python.org/library/re.html].

Unas últimas palabras de advertencia: las expresiones regulares son potentes, pero también es muy fácil equivocarse. calibre ofrece muy buenas posibilidades de pruebas para ver si las expresiones se comportan como se espera que lo hagan. Úselas. Intente no dispararse a los pies (me encanta esta expresión…). Pero si, a pesar de la advertencia, se lesiona el pie (o cualquier otra parte del cuerpo), trate de aprender de ello.

Referencia rápida

Referencia rápida para la sintaxis de expresiones regulares

Clases de caracteres

Clases de caracteres abreviadas

Los cuantifcadores

Avaricia

Disyuntiva

Exclusión

Anclajes

Grupos

Miradas alrededor

Recurrencia

Caracteres especiales

Metacaracteres

Modos

Créditos

Gracias por ayudar con sugerencias, correcciones y demás:

ldolse

kovidgoyal

chaley

dwanthny

kacir

Starson17

Orpheu

Para más información sobre expresiones regulares, véase The Python User Manual [https://docs.python.org/library/re.html] (en inglés). El motor de expresiones regulares usado por calibre es en realidad regex [https://bitbucket.org/mrabarnett/mrab-regex/src/hg/], que añade varias mejoras sobre el estándar de Python.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Todo acerca de cómo utilizar expresiones regulares en calibre »

Referencia rápida para la sintaxis de expresiones regulares

Referencia rápida para la sintaxis de expresiones regulares

Esta lista de verificación resume las partes más utilizadas o difíciles de recordar del motor de expresiones regulares disponible en la mayor parte de calibre.

Índice

Clases de caracteres

Clases de caracteres abreviadas

Los cuantifcadores

Avaricia

Disyuntiva

Exclusión

Anclajes

Grupos

Miradas alrededor

Recurrencia

Caracteres especiales

Metacaracteres

Modos

Clases de caracteres

Las clases de caracteres son útiles para representar diferentes grupos de caracteres de forma sucinta.

Ejemplos:

Representación

Clase

[a-z]

Letras minúsculas. No incluye caracteres con acentos ni ligaduras

[a-z0-9]

Letras minúsculas de la a la z o números de 0 a 9

[A-za-z-]

Letras mayúsculas o minúsculas, o un guión. Para incluir el guión en una clase debe ponerlo al principio o al final para que no se confunda con el guión que especifica una gama de caracteres

[^0-9]

Cualquier carácter excepto un dígito. El circunflejo (^) al principio de una clase excluye los caracteres de la clase (clase complementaria)

[[a-z]--[aeiouy]]

Las consonantes minúsculas. Una clase puede incluirse en otra calse. Los caracteres -- excluyen lo que va después

[\w--[\d_]]

Todas las letras (incluidos caracteres acentuados o extranjeros). Las clases abreviadas pueden usarse dentro de una clase

Ejemplo:

<[^<>]+> to select an HTML tag

Clases de caracteres abreviadas

Representación

Clase

\d

Un dígito (lo mismo que [0-9])

\D

Cualquier carácter no numérico (lo mismo que [^0-9])

\w

Un carácter alfanumérico ([a-zA-Z0-9]) incluidos caracteres con acentos y ligaduras

\W

Cualquier carácter de «no palabra»

\s

Espacio, espacio duro, tabulación, salto de línea

\S

Cualquier carácter «no blanco»

.

Cualquier carácter excepto un salto de línea. Use la casilla «multilínea» o el modificar de expresión regular (?s) para incluir el salto de línea.

Los cuantifcadores

Cuantificador

Número de repeticiones de la expresión que precede al cuantificador

?

0 o 1 repeticiones de la expresión. Lo mismo que {0,1}

+

1 o más repeticiones de la expresión. Lo mismo que {1,}

*

0, 1 o más repeticiones de la expresión. Lo mismo que {0,}

{n}

Exactamente n repeticiones de la expresión

{mín,max}

Número de repeticiones entre el mínimo y el máximo, ambos inclusive

{mín,}

Número de repeticiones entre el mínimo, inclusive, y el infinito

{,max}

Número de repeticiones entr 0 y el máximo, inclusive

Avaricia

Con cuantificadores, de manera predeterminada, el motor de expresiones regulares es avaricioso: extiende la selección tanto como sea posible. Esto suele ocasionar sorpresas al principio. Se puede añadir ? a un cuantificador para hacerlo perezoso. Procure no poner dos en una misma expresión, el resultado puede ser impredecible.

Tenga cuidado con cuantificadores anidados, como el patrón (a*)*, ya que aumentan el tiempo de proceso exponencialmente.

Disyuntiva

El carácter | en una expresión regular es un O lógico. Significa que puede coincidir bien la expresión que lo precede o bien la que lo sigue.

Exclusión

Método 1

patrón_para_excluir(*SKIP)(*FAiL)|patrón_para_seleccionar

Ejemplo:

"Blabla"(*SKIP)(*FAIL)|Blabla

selecciona Blabla en los textos Blabla, "Blabla o Blabla", pero no en "Blabla".

Método 2

patrón_para_excluir\K|(patrón_para_seleccionar)

"Blabla"\K|(Blabla)

selecciona Blabla en los textos Blabla, "Blabla o Blabla", pero no en "Blabla".

Anclajes

Un anclaje es una forma de seleccionar una ubicación lógica en un texto, en lugar de un carácter. Los anclajes más útiles para el procesado de textos son:

\b

Indica una frontera de palabra, es decir la transición de carácter espaciador a carácter no espaciador. Por ejemplo, puede usar \brata para seleccionar la rata pero no catarata.

^

Coincide con el principio de una línea (en modo multilínea, que es el predeterminado)

$

Coincide con el final de una línea (en modo multilínea, que es el predeterminado)

\K

Restablece la posición de inicio de la selección a su posición en el patrón. Algunos motores de expresiones regulares (pero no calibre) no permiten «mirar atrás» con longitud variable, especialmente con cuantificadores. Cuando puede usar \K con estos motores, también le permite obviar este límite escribiendo el equivalente de una «mirada atrás» positiva con longitud variable.

Grupos

(expresión)

Grupo de captura, que almacena la selección y puede recuperarse después en los patrónes de búsqueda o sustitución con \n, donde n es el número de secuencia del grupo de captura (empezando por 1 en orden de lectura)

(?:expresión)

Grupo que no captura la selección

(?>expresión)

Grupo atómico: En cuanto la expresión se satisface, el motor de expresiones regulares continua y, si el resto del patrón falla, no volverá atrás para probar otras combinaciones con la expresión. Los grupos atómicos no capturan.

(?|expresión)

Grupo de restablecimiento de rama: las ramas en las disyuntivas incluidas en la expresión comparten los mismo números de grupo

(?<nombre>expresión)

Grupo llamado «nombre». La selección pude recuperarse después en el patrón de búsqueda con (?P=name) y en el sustitución con \g<nombre>. Dos grupos diferentes pueden usar el mismo nombre.

Miradas alrededor

Mirada

Significado

?=

Mirada adelante positiva (se pone después de la selección)

?!

Mirada adelante negativa (se pone después de la selección)

?<=

Mirada atrás positiva (se pone antes de la selección)

?<!

Mirada atrás negativa (se pone antes de la selección)

Las miradas adelante y atrás no consumen caracteres, tienen longitud cero y no capturan. Son grupos atómicos: en cuanto la expresión se satisface, el motor de expresiones regulares continua y, si el resto del patrón falla, no volverá al interior de la mirada para probar otras combinaciones.

Al buscar coincidencias múltiples en un texto, al inicio de cada intento de coincidencia una mirada atrás puede inspeccionar los caracteres anteriores a la posición actual. Por tanto, en el texto 123, el patrón (?<=\d)\d (un dígito precedido por un dígito) debería, en teoría, seleccionar 2 y 3. Por otro lado, \d\K\d sólo puede seleccionar 2, porque la posición de inicio después de la primera selección es justo delante del 3 y no hay suficientes dígitos para una segunda coincidencia. Análogamente, \d(\d) sólo captura 2. En la práctica del motor de expresiones regulares de calibre, la mirada atrás positiva se comporta de la misma manera y selecciona sólo 2, en contra de la teoría.

Pueden incluirse grupos en las miradas, pero la captura no suele ser útil. No obstante, si es útil será necesario ser muy cuidadoso con el uso de cuantificadores en una mirada atrás: la avaricia unida a la ausencia de retorno puede dar lugar a una captura sorprendente. Por este motivo, use \K en lugar de una mirada atrás positiva cuando tenga un cuantificador (o peor, varios) en un grupo de captura dentro de la mirada atrás positiva.

Ejemplo de mirada adelante negativa:

(?![^<>{}]*[>}])

Colocada al final del patrón, evita seleccionar dentro de una etiqueta o un estilo incluido en el archivo.

Siempre que sea posible, es mejor «anclar» las miradas para reducir el número de pasos necesarios para obtener el resultado.

Recurrencia

Representación

Significado

(?R)

Recurrencia de todo el patrón

(?1)

Recurrencia del único patrón del grupo de captura numerado, aquí el grupo 1

Recurrencia es ejecutarse a sí mismo. Esto es útil para consultas equilibradas, como textos entre comillas que pueden contener textos entrecomillados. Así, si durante el procesado de un texto entre comillas dobles encontramos el inicio de un nuevo texto entre comillas dobles, el patrón sabrá qué hacer y se ejecuta a sí mismo. Tendremos un patrón como:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

Para seleccionar un texto entre comillas dobles sin detenerse en un texto interior:

“((?>[^“”]+|(?R))*[^“”]+)”

Esta plantilla también puede usarse para modificar pares de etiquetas que pueden anidarse, como etiquetas <div>.

Caracteres especiales

Representación

Carácter

\t

tabulación

\n

salto de línea

\x20

espacio (blando)

\xa0

espacio duro

Metacaracteres

Los metacaracteres son caracteres que tienen un significado especial para el motor de expresiones regulares. De estos, doce deben ir precedidos de un carácter de escape, la barra invertida (\), para perder su significado especial y volver a ser caracteres normales:

^ . [ ] $ ( ) * + ? | \

Otros siete metacaracteres no necesitan ir precedidos de una barra invertida (pero pueden ir, sin ninguta otra consecuencia):

{ } ! < > = :

Los caracteres especiales pierden su condición si se usan dentro de una clase (entre corchetes []). El corchete de cierre y el guión tienen un estatus especial dentro de una clase. Fuera de la clase el guión es un simple guión literal, el corchete de cierre sigue siendo un metacarácter.

La barra (/) y la almohadilla (o signo de número) (#) no son metacaracteres, no necesitan escaparse.

En algunas herramientas, como regex101.com con el motor Python, las comillas dobles tienen un estatus especial como separador, y deben escaparse, o deben cambiarse las opciones. Éste no es el caso en el editor de calibre.

Modos

(?s)

Hace que el punto (.) coincida con caracteres de salto de línea también

(?m)

Hace que los anclajes ^ y $ coincidan con el principio y final de líneas en lugar del principio y final del texto completo.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Escribir sus propios complementos para extender la funcionalidad de calibre

Escribir sus propios complementos para extender la funcionalidad de calibre

calibre tiene un diseño muy modular. Casi todas las funciones de calibre vienen en forma de complementos. Los complementos se utilizan para la conversión, para la descarga de noticias (aunque éstos se llaman fórmulas), para diversos componentes de la interfaz de usuario, para conectarse a diferentes dispositivos, para procesar archivos cuando se añaden a calibre, etcétera. Puede obtener una lista completa de todos los complementos integrados en calibre en Preferencias > Avanzado > Complementos.

Aquí le enseñaremos como crear sus propios complementos para agregar funciones a calibre.

Índice

Anatomía de un complemento de calibre

Un complemento de interfaz de usuario

__init__.py

ui.py

main.py

Obtener recursos del archivo ZIP del complemento

Habilitar la configuración de usuario para el complemento

Complementos para modificar libros

main.py

Añadir traducciones al complemento

La API del complemento

Depurar complementos

Más ejemplos de complementos

Compartir sus complementos con otros

Nota

Esto sólo se aplica a versiones calibre >= 0.8.60

Anatomía de un complemento de calibre

Un complemento de calibre es muy sencillo, no es más que un archivo ZIP que contiene algo de código Python y otros recursos, como archivos de imagen, que necesite el complemento. Sin más preámbulos, vamos a ver un ejemplo básico.

Supongamos que está usando una instalación de calibre publicar sus propios documentos electrónicos en formatos EPUB y MOBI. Le gustaría que todos los archivos generados por calibre establecieran la editorial como «Hello World», he aquí cómo hacerlo. Cree un archivo llamado __init __.py (éste es un nombre especial y siempre debe ser el del archivo principal del complemento) e introduzca el siguiente código Python en él:

from calibre.customize import FileTypePlugin

class HelloWorld(FileTypePlugin):

    name                = 'Hello World Plugin' # Name of the plugin
    description         = 'Set the publisher to Hello World for all new conversions'
    supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
    author              = 'Acme Inc.' # The author of this plugin
    version             = (1, 0, 0)   # The version number of this plugin
    file_types          = {'epub', 'mobi'} # The file types that this plugin will be applied to
    on_postprocess      = True # Run this plugin after conversion is complete
    minimum_calibre_version = (0, 7, 53)

    def run(self, path_to_ebook):
        from calibre.ebooks.metadata.meta import get_metadata, set_metadata
        with open(path_to_ebook, 'r+b') as file:
            ext  = os.path.splitext(path_to_ebook)[-1][1:].lower()
            mi = get_metadata(file, ext)
            mi.publisher = 'Hello World'
            set_metadata(file, mi, ext)
        return path_to_ebook

Eso es todo. Para agregar este código a calibre como un complemento, simplemente ejecute lo siguiente en la carpeta donde reside el archivo __init__.py:

calibre-customize -b .

Nota

En macOS, las herramientas de línea de órdenes están dentro del paquete calibre, por ejemplo, si ha instalado calibre en /Aplicaciones las herramientas de línea de órdenes están en :file:/Aplicaciones/calibre.app/Contents/MacOS/`.

Puede descargar el complemento Hello World de helloworld_plugin.zip [https://calibre-ebook.com/downloads/helloworld_plugin.zip].

Cada vez que utilice calibre para convertir un libro, el método run() del complemento se ejecutará y el libro convertido tendrá como editorial predeterminada «Hello World». Éste es un complemento intranscendente, veamos un ejemplo más complejo que agrega un componente a la interfaz de usuario.

Un complemento de interfaz de usuario

Este complemento ocupará unos pocos archivos (para mantener el código más limpio). Le mostrará cómo obtener recursos (imágenes o archivos de datos) desde el archivo ZIP del complemento, cómo permitir a los usuarios configurar el complemento, cómo crear elementos en la interfaz de usuario de calibre y cómo acceder y consultar la base de datos de libros de calibre.

Puede descargar este complemento de interface_demo_plugin.zip [https://calibre-ebook.com/downloads/interface_demo_plugin.zip]

Lo primero que hay que tener en cuenta es que este archivo ZIP tiene muchos más archivos en su interior, explicados a continuación, preste especial atención a plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Un archivo de texto vacío utilizado para activar la gestión de múltiples archivos en el complemento. Este archivo debe estar presente en todos los complementos que utilicen más de un archivo .py. Debe quedar vacío y su nombre debe ser de la forma: plugin-import-name-**some_name**.txt. La presencia de este archivo le permite importar código de los archivos .py presentes dentro del archivo ZIP, usando una sentencia como ésta:

from calibre_plugins.some_name.some_module import some_object

El prefijo calibre_plugins siempre debe estar presente. some_name proviene del nombre del archivo de texto vacío. some_module se refiere al archivo some_module.py dentro del archivo ZIP. Tenga en cuenta que esta importación es tan potente como las importaciones normales en Python. Puede crear paquetes y subpaquetes de módulos .py dentro del archivo ZIP, igual que lo haría normalmente (definiendo __init__.py en cada subcarpeta), y todo «debería funcionar».

El nombre que utilice para some_name entra en un espacio global de nombres compartido por todos los complementos, así que hágalo tan único como sea posible. Pero recuerde que debe ser un identificador Python válido (sólo letras, números y guión bajo).

__init__.py

Como antes, el archivo que define la clase del complemento

main.py

Este archivo contiene el código real que realiza alguna operación útil

ui.py

Este archivo define la interfaz del complemento

images/icon.png

El icono para este complemento

about.txt

Un archivo de texto con información acerca de este complemento

translations

Una carpeta que contiene archivos .mo con las traducciones de la interfaz de usuario del complemento a diferentes idiomas. Ver más adelante para más detalles.

Ahora veamos el código.

__init__.py

Primero, el __init__.py obligatorio para definir los metadatos del complemento:

from calibre.customize import InterfaceActionBase

class InterfacePluginDemo(InterfaceActionBase):
    '''
    This class is a simple wrapper that provides information about the actual
    plugin class. The actual interface plugin class is called InterfacePlugin
    and is defined in the ui.py file, as specified in the actual_plugin field
    below.

    The reason for having two classes is that it allows the command line
    calibre utilities to run without needing to load the GUI libraries.
    '''
    name                = 'Interface Plugin Demo'
    description         = 'An advanced plugin demo'
    supported_platforms = ['windows', 'osx', 'linux']
    author              = 'Kovid Goyal'
    version             = (1, 0, 0)
    minimum_calibre_version = (0, 7, 53)

    #: This field defines the GUI plugin class that contains all the code
    #: that actually does something. Its format is module_path:class_name
    #: The specified class must be defined in the specified module.
    actual_plugin       = 'calibre_plugins.interface_demo.ui:InterfacePlugin'

    def is_customizable(self):
        '''
        This method must return True to enable customization via
        Preferences->Plugins
        '''
        return True

    def config_widget(self):
        '''
        Implement this method and :meth:`save_settings` in your plugin to
        use a custom configuration dialog.

        This method, if implemented, must return a QWidget. The widget can have
        an optional method validate() that takes no arguments and is called
        immediately after the user clicks OK. Changes are applied if and only
        if the method returns True.

        If for some reason you cannot perform the configuration at this time,
        return a tuple of two strings (message, details), these will be
        displayed as a warning dialog to the user and the process will be
        aborted.

        The base class implementation of this method raises NotImplementedError
        so by default no user configuration is possible.
        '''
        # It is important to put this import statement here rather than at the
        # top of the module as importing the config class will also cause the
        # GUI libraries to be loaded, which we do not want when using calibre
        # from the command line
        from calibre_plugins.interface_demo.config import ConfigWidget
        return ConfigWidget()

    def save_settings(self, config_widget):
        '''
        Save the settings specified by the user with config_widget.

        :param config_widget: The widget returned by :meth:`config_widget`.
        '''
        config_widget.save_settings()

        # Apply the changes
        ac = self.actual_plugin_
        if ac is not None:
            ac.apply_settings()

La única característica notable es el campo actual_plugin. Puesto que calibre posee tanto una interfaz gráfica como de línea de órdenes, los complementos con interfaz gráfica como éste no cargan ninguna biblioteca gráfica en __init__.py. El campo actual_plugin se encargad de esto, informando a calibre de que el complemento real se encuentra en otro archivo dentro del archivo ZIP, que sólo se cargará en un contexto de interfaz gráfica.

Recuerde que para que esto funcione, debe tener un archivo plugin-import-name-some_name.txt en el archivo ZIP del complemento, como se discutió anteriormente.

También hay un par de métodos para permitir la configuración por parte del usuario del complemento. Éstos se discuten más adelante.

ui.py

Veamos ahora ui.py, que define la interfaz gráfica del complemento. El código fuente está explícitamente comentado y se explica por sí mismo:

from calibre.gui2.actions import InterfaceAction
from calibre_plugins.interface_demo.main import DemoDialog

class InterfacePlugin(InterfaceAction):

    name = 'Interface Plugin Demo'

    # Declare the main action associated with this plugin
    # The keyboard shortcut can be None if you dont want to use a keyboard
    # shortcut. Remember that currently calibre has no central management for
    # keyboard shortcuts, so try to use an unusual/unused shortcut.
    action_spec = ('Interface Plugin Demo', None,
            'Run the Interface Plugin Demo', 'Ctrl+Shift+F1')

    def genesis(self):
        # This method is called once per plugin, do initial setup here

        # Set the icon for this interface action
        # The get_icons function is a builtin function defined for all your
        # plugin code. It loads icons from the plugin zip file. It returns
        # QIcon objects, if you want the actual data, use the analogous
        # get_resources builtin function.
        #
        # Note that if you are loading more than one icon, for performance, you
        # should pass a list of names to get_icons. In this case, get_icons
        # will return a dictionary mapping names to QIcons. Names that
        # are not found in the zip file will result in null QIcons.
        icon = get_icons('images/icon.png', 'Interface Demo Plugin')

        # The qaction is automatically created from the action_spec defined
        # above
        self.qaction.setIcon(icon)
        self.qaction.triggered.connect(self.show_dialog)

    def show_dialog(self):
        # The base plugin object defined in __init__.py
        base_plugin_object = self.interface_action_base_plugin
        # Show the config dialog
        # The config dialog can also be shown from within
        # Preferences->Plugins, which is why the do_user_config
        # method is defined on the base plugin class
        do_user_config = base_plugin_object.do_user_config

        # self.gui is the main calibre GUI. It acts as the gateway to access
        # all the elements of the calibre user interface, it should also be the
        # parent of the dialog
        d = DemoDialog(self.gui, self.qaction.icon(), do_user_config)
        d.show()

    def apply_settings(self):
        from calibre_plugins.interface_demo.config import prefs
        # In an actual non trivial plugin, you would probably need to
        # do something based on the settings in prefs
        prefs

main.py

El código que implementa el cuadro de diálogo «Interface Plugin Demo».

from qt.core import QDialog, QLabel, QMessageBox, QPushButton, QVBoxLayout

class DemoDialog(QDialog):

    def __init__(self, gui, icon, do_user_config):
        QDialog.__init__(self, gui)
        self.gui = gui
        self.do_user_config = do_user_config

        # The current database shown in the GUI
        # db is an instance of the class LibraryDatabase from db/legacy.py
        # This class has many, many methods that allow you to do a lot of
        # things. For most purposes you should use db.new_api, which has
        # a much nicer interface from db/cache.py
        self.db = gui.current_db

        self.l = QVBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel(prefs['hello_world_msg'])
        self.l.addWidget(self.label)

        self.setWindowTitle('Interface Plugin Demo')
        self.setWindowIcon(icon)

        self.about_button = QPushButton('About', self)
        self.about_button.clicked.connect(self.about)
        self.l.addWidget(self.about_button)

        self.marked_button = QPushButton(
            'Show books with only one format in the calibre GUI', self)
        self.marked_button.clicked.connect(self.marked)
        self.l.addWidget(self.marked_button)

        self.view_button = QPushButton(
            'View the most recently added book', self)
        self.view_button.clicked.connect(self.view)
        self.l.addWidget(self.view_button)

        self.update_metadata_button = QPushButton(
            'Update metadata in a book\'s files', self)
        self.update_metadata_button.clicked.connect(self.update_metadata)
        self.l.addWidget(self.update_metadata_button)

        self.conf_button = QPushButton(
                'Configure this plugin', self)
        self.conf_button.clicked.connect(self.config)
        self.l.addWidget(self.conf_button)

        self.resize(self.sizeHint())

    def about(self):
        # Get the about text from a file inside the plugin zip file
        # The get_resources function is a builtin function defined for all your
        # plugin code. It loads files from the plugin zip file. It returns
        # the bytes from the specified file.
        #
        # Note that if you are loading more than one file, for performance, you
        # should pass a list of names to get_resources. In this case,
        # get_resources will return a dictionary mapping names to bytes. Names that
        # are not found in the zip file will not be in the returned dictionary.
        text = get_resources('about.txt')
        QMessageBox.about(self, 'About the Interface Plugin Demo',
                text.decode('utf-8'))

    def marked(self):
        ''' Show books with only one format '''
        db = self.db.new_api
        matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
        # Mark the records with the matching ids
        # new_api does not know anything about marked books, so we use the full
        # db object
        self.db.set_marked_ids(matched_ids)

        # Tell the GUI to search for all marked records
        self.gui.search.setEditText('marked:true')
        self.gui.search.do_search()

    def view(self):
        ''' View the most recently added book '''
        most_recent = most_recent_id = None
        db = self.db.new_api
        for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
            if most_recent is None or timestamp > most_recent:
                most_recent = timestamp
                most_recent_id = book_id

        if most_recent_id is not None:
            # Get a reference to the View plugin
            view_plugin = self.gui.iactions['View']
            # Ask the view plugin to launch the viewer for row_number
            view_plugin._view_calibre_books([most_recent_id])

    def update_metadata(self):
        '''
        Set the metadata in the files in the selected book's record to
        match the current metadata in the database.
        '''
        from calibre.ebooks.metadata.meta import set_metadata
        from calibre.gui2 import error_dialog, info_dialog

        # Get currently selected books
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, 'Cannot update metadata',
                             'No books selected', show=True)
        # Map the rows to book ids
        ids = list(map(self.gui.library_view.model().id, rows))
        db = self.db.new_api
        for book_id in ids:
            # Get the current metadata for this book from the db
            mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
            fmts = db.formats(book_id)
            if not fmts:
                continue
            for fmt in fmts:
                fmt = fmt.lower()
                # Get a python file object for the format. This will be either
                # an in memory file or a temporary on disk file
                ffile = db.format(book_id, fmt, as_file=True)
                ffile.seek(0)
                # Set metadata in the format
                set_metadata(ffile, mi, fmt)
                ffile.seek(0)
                # Now replace the file in the calibre library with the updated
                # file. We dont use add_format_with_hooks as the hooks were
                # already run when the file was first added to calibre.
                db.add_format(book_id, fmt, ffile, run_hooks=False)

        info_dialog(self, 'Updated files',
                'Updated the metadata in the files of %d book(s)'%len(ids),
                show=True)

    def config(self):
        self.do_user_config(parent=self)
        # Apply the changes
        self.label.setText(prefs['hello_world_msg'])

Obtener recursos del archivo ZIP del complemento

El sistema de carga de complementos de calibre tiene predefinidas un par de funciones que permiten obtener archivos desde el archivo ZIP del complemento de manera práctica.

get_resources(nombre_o_lista_de_nombres)

Esta función debe ejecutarse con una lista de rutas de acceso a archivos dentro del archivo ZIP. Por ejemplo, para acceder al archivo icon.png en la carpeta images del archivo ZIP, utilizaría: images/icon.png. Use siempre una barra inclinada a la derecha como separador de ruta, incluso en windows. Cuando se pasa un solo nombre, la función devolverá los bytes en bruto de ese archivo o None si el nombre no se encuentra en el archivo ZIP. Si se pasa más de un nombre, entonces devuelve un diccionario de mapeo de nombres a bytes. Si no se encuentra un nombre, no estará presente en el diccionario devuelto.

get_icons(nombre_o_lista_de_nombres, nombre_de_complemento)

Una extensión de get_resources() que crea objetos QIcon a partir de los bytes en bruto devueltos por get_resources. Si un nombre no se encuentra en el archivo ZIP, el correspondiente QIcon será nulo. Para permitir el uso de temas de iconos, pase el nombre «para humanos» del complemento como nombre de complemento. Si el usuario está usando un tema de iconos con iconos para el complemento, se usarán éstos de manera preferente.

Habilitar la configuración de usuario para el complemento

Para permitir a los usuarios configurar el complemento, debe definir tres métodos en la clase base del complemento, is_customizable, config_widget y save_settings como se muestra a continuación:

    def is_customizable(self):
        '''
        This method must return True to enable customization via
        Preferences->Plugins
        '''
        return True

    def config_widget(self):
        '''
        Implement this method and :meth:`save_settings` in your plugin to
        use a custom configuration dialog.

        This method, if implemented, must return a QWidget. The widget can have
        an optional method validate() that takes no arguments and is called
        immediately after the user clicks OK. Changes are applied if and only
        if the method returns True.

        If for some reason you cannot perform the configuration at this time,
        return a tuple of two strings (message, details), these will be
        displayed as a warning dialog to the user and the process will be
        aborted.

        The base class implementation of this method raises NotImplementedError
        so by default no user configuration is possible.
        '''
        # It is important to put this import statement here rather than at the
        # top of the module as importing the config class will also cause the
        # GUI libraries to be loaded, which we do not want when using calibre
        # from the command line
        from calibre_plugins.interface_demo.config import ConfigWidget
        return ConfigWidget()

    def save_settings(self, config_widget):
        '''
        Save the settings specified by the user with config_widget.

        :param config_widget: The widget returned by :meth:`config_widget`.
        '''
        config_widget.save_settings()

        # Apply the changes
        ac = self.actual_plugin_
        if ac is not None:
            ac.apply_settings()

calibre tiene muchas maneras diferentes de almacenar los datos de configuración (un legado de su larga historia). El método recomendado es usar la clase JSONConfig, que almacena la información de configuración en un archivo .json.

El código para gestionar los datos de configuración en el complemento de demostración está en config.py:

from qt.core import QHBoxLayout, QLabel, QLineEdit, QWidget

# This is where all preferences for this plugin will be stored
# Remember that this name (i.e. plugins/interface_demo) is also
# in a global namespace, so make it as unique as possible.
# You should always prefix your config file name with plugins/,
# so as to ensure you dont accidentally clobber a calibre config file
prefs = JSONConfig('plugins/interface_demo')

# Set defaults
prefs.defaults['hello_world_msg'] = 'Hello, World!'

class ConfigWidget(QWidget):

    def __init__(self):
        QWidget.__init__(self)
        self.l = QHBoxLayout()
        self.setLayout(self.l)

        self.label = QLabel('Hello world &message:')
        self.l.addWidget(self.label)

        self.msg = QLineEdit(self)
        self.msg.setText(prefs['hello_world_msg'])
        self.l.addWidget(self.msg)
        self.label.setBuddy(self.msg)

    def save_settings(self):
        prefs['hello_world_msg'] = self.msg.text()

El objeto prefs está ahora disponible en todo el código del complemento simplemente con:

from calibre_plugins.interface_demo.config import prefs

Puede observar que el objeto prefs se usa en main.py:

        self.do_user_config(parent=self)
        # Apply the changes
        self.label.setText(prefs['hello_world_msg'])

Complementos para modificar libros

Vamos a cambiar de tercio y enfocarnos en la creación de un complemento para añadir herramientas al editor de libros de calibre. El complemento está disponible aquí: editor_demo_plugin.zip [https://calibre-ebook.com/downloads/editor_demo_plugin.zip].

El primer paso, como para todos los complementos es crear el archivo vacío con el nombre de importación descrito anteriormente. Vamos a nombrar el archivo plugin-import-name-editor_plugin_demo.txt.

Ahora creamos el archivo obligatorio __init__.py que contiene los metadatos del complemento: nombre, autor, versión, etc.

from calibre.customize import EditBookToolPlugin

class DemoPlugin(EditBookToolPlugin):

    name = 'Edit Book plugin demo'
    version = (1, 0, 0)
    author = 'Kovid Goyal'
    supported_platforms = ['windows', 'osx', 'linux']
    description = 'A demonstration of the plugin interface for the ebook editor'
    minimum_calibre_version = (1, 46, 0)

Un solo complemento del editor puede proporcionar múltiples herramientas; cada herramienta corresponde a un único botón en la barra de herramientas y entrada en el menú Complementos del editor. Éstos pueden tener submenús en el caso de que la herramienta posea múltiples acciones relacionadas.

Todas las herramientas deben estar definidas en el archivo main.py del complemento. Cada herramienta es una clase que hereda de la clase calibre.gui2.tweak_book.plugin.Tool. Echemos un vistazo al main.py del complemento de demostración; el código fuente está profusamente comentado y se explica por sí mismo. Lea la documentación de la API de la clase :clase:`calibre.gui2.tweak_book.plugin.Tool` para más detalles.

main.py

Aquí veremos la definición de una herramienta que multiplicará todos los tamaños de letra en el libro por un número proporcionado por el usuario. Esta herramienta demuestra varios conceptos importantes que se necesitarán para desarrollar otros complementos, por lo que debe leer el código fuente (muy comentado) cuidadosamente.

import re

from calibre import force_unicode
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, serialize
from calibre.gui2 import error_dialog

# The base class that all tools must inherit from
from calibre.gui2.tweak_book.plugin import Tool
from css_parser.css import CSSRule
from qt.core import QAction, QInputDialog

class DemoTool(Tool):

    #: Set this to a unique name it will be used as a key
    name = 'demo-tool'

    #: If True the user can choose to place this tool in the plugins toolbar
    allowed_in_toolbar = True

    #: If True the user can choose to place this tool in the plugins menu
    allowed_in_menu = True

    def create_action(self, for_toolbar=True):
        # Create an action, this will be added to the plugins toolbar and
        # the plugins menu
        ac = QAction(get_icons('images/icon.png'), 'Magnify fonts', self.gui)  # noqa
        if not for_toolbar:
            # Register a keyboard shortcut for this toolbar action. We only
            # register it for the action created for the menu, not the toolbar,
            # to avoid a double trigger
            self.register_shortcut(ac, 'magnify-fonts-tool', default_keys=('Ctrl+Shift+Alt+D',))
        ac.triggered.connect(self.ask_user)
        return ac

    def ask_user(self):
        # Ask the user for a factor by which to multiply all font sizes
        factor, ok = QInputDialog.getDouble(
            self.gui, 'Enter a magnification factor', 'Allow font sizes in the book will be multiplied by the specified factor',
            value=2, min=0.1, max=4
        )
        if ok:
            # Ensure any in progress editing the user is doing is present in the container
            self.boss.commit_all_editors_to_container()
            try:
                self.magnify_fonts(factor)
            except Exception:
                # Something bad happened report the error to the user
                import traceback
                error_dialog(self.gui, _('Failed to magnify fonts'), _(
                    'Failed to magnify fonts, click "Show details" for more info'),
                    det_msg=traceback.format_exc(), show=True)
                # Revert to the saved restore point
                self.boss.revert_requested(self.boss.global_undo.previous_container)
            else:
                # Show the user what changes we have made, allowing her to
                # revert them if necessary
                self.boss.show_current_diff()
                # Update the editor UI to take into account all the changes we
                # have made
                self.boss.apply_container_update_to_gui()

    def magnify_fonts(self, factor):
        # Magnify all font sizes defined in the book by the specified factor
        # First we create a restore point so that the user can undo all changes
        # we make.
        self.boss.add_savepoint('Before: Magnify fonts')

        container = self.current_container  # The book being edited as a container object

        # Iterate over all style declarations in the book, this means css
        # stylesheets, <style> tags and style="" attributes
        for name, media_type in container.mime_map.items():
            if media_type in OEB_STYLES:
                # A stylesheet. Parsed stylesheets are css_parser CSSStylesheet
                # objects.
                self.magnify_stylesheet(container.parsed(name), factor)
                container.dirty(name)  # Tell the container that we have changed the stylesheet
            elif media_type in OEB_DOCS:
                # A HTML file. Parsed HTML files are lxml elements

                for style_tag in container.parsed(name).xpath('//*[local-name="style"]'):
                    if style_tag.text and style_tag.get('type', None) in {None, 'text/css'}:
                        # We have an inline CSS <style> tag, parse it into a
                        # stylesheet object
                        sheet = container.parse_css(style_tag.text)
                        self.magnify_stylesheet(sheet, factor)
                        style_tag.text = serialize(sheet, 'text/css', pretty_print=True)
                        container.dirty(name)  # Tell the container that we have changed the stylesheet
                for elem in container.parsed(name).xpath('//*[@style]'):
                    # Process inline style attributes
                    block = container.parse_css(elem.get('style'), is_declaration=True)
                    self.magnify_declaration(block, factor)
                    elem.set('style', force_unicode(block.getCssText(separator=' '), 'utf-8'))

    def magnify_stylesheet(self, sheet, factor):
        # Magnify all fonts in the specified stylesheet by the specified
        # factor.
        for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
            self.magnify_declaration(rule.style, factor)

    def magnify_declaration(self, style, factor):
        # Magnify all fonts in the specified style declaration by the specified
        # factor
        val = style.getPropertyValue('font-size')
        if not val:
            return
        # see if the font-size contains a number
        num = re.search(r'[0-9.]+', val)
        if num is not None:
            num = num.group()
            val = val.replace(num, '%f' % (float(num) * factor))
            style.setProperty('font-size', val)
        # We should also be dealing with the font shorthand property and
        # font sizes specified as non numbers, but those are left as exercises
        # for the reader

Vamos a analizar main.py. Vemos que define una única herramienta, llamada Magnify fonts. Esta herramienta le pediráá al usuario un número y multiplicará todos los tamaños de letra en el libro por dicho número.

La primera cosa importante es el nombre de la herramienta que debe establecer a algún texto relativamente único, ya que se utilizará como clave para esta herramienta.

El siguiente punto de entrada importante es calibre.gui2.tweak_book.plugin.Tool.create_action(). Este método crea los objetos QAction que aparecen en la barra de herramientas y en el menú de complementos. También, opcionalmente, asigna un atajo de teclado que el usuario puede personalizar. La señal que se genera en el objeto QAction está conectada con el método ask_user() que pide al usuario el multiplicador del tamaño de letra, y luego ejecuta el código de ampliación.

El código de aumento está bien comentado y es bastante simple. Lo principal que hay que destacar es que se obtiene una referencia a la ventana del editor como self.gui y al Boss del editor como self.boss. El Boss es el objeto que controla la interfaz de usuario del editor. Tiene muchos métodos útiles, que se documentan en la clase calibre.gui2.tweak_book.boss.Boss.

Finalmente está self.current_container, que es una referencia al libro que se está editando como un objeto calibre.ebooks.oeb.polish.container.Container. Éste representa el libro como una colección de archivos HTML, CSS e imágenes y posee diversos métodos prácticos para hacer varias cosas útiles. El objeto contenedor y varias funciones utilitarias que se pueden reutilizar en el código de los complementos están documentados en Documentación de la API para las herramientas de modificación de libros electrónicos.

Añadir traducciones al complemento

Puede traducir todos los textos de la interfaz de usuario del complemento y mostrarlos en el idioma en que está configurada la interfaz principal de calibre.

El primer paso es ir al código fuente del complemento y marcar todos los textos visibles por el usuario como traducibles, encerrándolos con _(). Por ejemplo:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Después utilice algún programa para generar archivos .po para el código fuente del complemento. Debe haber un archivo .po para cada idioma al que lo quiera traducir. Por ejemplo: de.po para el alemán, fr.po para el francés, etc. Puede utilizar el programa Poedit [https://poedit.net/] para esto.

Envíe estos archivos .po a los traductores. Cuando reciba los archivos traducidos, compílelos en archivos .mo. Puede utilizar nuevamente Poedit para ello, o simplemente ejecutar:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Ponga los archivos .mo en la carpeta translations del complemento.

El último paso es simplemente ejecutar la función load_translations() al principio de los archivos .py del complemento. Por motivos de rendimiento, sólo debe llamar a esta función en aquellos archivos .py que realmente poseen textos traducibles. Así que en un complemento de interfaz de usuario típico, la ejecutaría en ui.py pero no en __init__.py.

Puede probar las traducciones del complemento cambiando el idioma de la interfaz de usuario en calibre bajo Preferencias > Interfaz > Apariencia o ejecutando calibre con la variable de entorno CALIBRE_OVERRIDE_LANG establecida. Por ejemplo:

CALIBRE_OVERRIDE_LANG=de

Sustituya de por el código del idioma que desea probar.

Para traducciones con plurales, use la función ngettext() en lugar de _(). Por ejemplo:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

La API del complemento

Como ya se habrá dado cuenta, un complemento en calibre es una clase. Hay diferentes clases para los diferentes tipos de complementos en calibre. Los detalles de cada clase, incluyendo la clase base de todos los complementos, se pueden encontrar en Documentación de la API para complementos.

Es casi seguro que el complemento va a usar el código de calibre. Para saber cómo encontrar los distintos elementos de funcionalidad en el código base de calibre, lea la sección Estructura del código.

Depurar complementos

El primer paso, el más importante, es ejecutar calibre en modo de depuración. Puede hacer esto desde la línea de órdenes con:

calibre-debug -g

O desde el mismo calibre, pulsando con el botón derecho en Preferencias o usando el atajo de teclado Ctrl+Mayús+R.

Cuando se ejecuta desde la línea de órdenes, la salida de depuración se enviará a la consola, si se ejecuta dentro de calibre, la salida irá a un archivo txt.

Puede insertar sentencias de impresión en cualquier lugar en el código fuente del complemento, tendrán efecto en el modo de depuración. Recuerde, esto es Python, no debe necesitar más que sentencias de impresión para depurar ;) He desarrollado todo calibre usando sólo esta técnica de depuración.

Puede probar rápidamente los cambios en el complemento con la siguiente orden:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Esto cerrará calibre, esperará hasta que se cierre completamente, después actualiza el complemento en calibre y vuelve a iniciar calibre.

Más ejemplos de complementos

Puede encontrar una lista de muchos complementos de calibre más complejos aqui [https://www.mobileread.com/forums/showthread.php?t=118764].

Compartir sus complementos con otros

Si desea compartir los complementos que ha creado con otros usuarios de calibre, inicie un nuevo hilo adjuntando el complemento en el foros de complementos de calibre [https://www.mobileread.com/forums/forumdisplay.php?f=237] (en inglés).

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Escribir fórmulas matemáticas en libros electrónicos

Escribir fórmulas matemáticas en libros electrónicos

El visor de libros de calibre tiene la capacidad de mostrar fórmulas matemáticas incrustadas en libros electrónicos (archivos EPUB y HTML). Puede escribir las fórmulas directamente con TeX, MathML o ASCIIMath. El visor de calibre usa la excelente biblioteca MathJax [https://www.mathjax.org] para procesar las fórmulas. Esto es un breve cursillo para crear libros con fórmulas que funcionen con el visor de calibre.

Un archivo HTML sencillo con fórmulas

Puede escribir fórmulas matemáticas directamente en el texto de un archivo HTML sencillo y el visor de libros de calibre las mostrará con una composición matemática adecuada. En el ejemplo de abajo, usamos la notación de TeX para las fórmulas. Verá que puede usar las órdenes normales de TeX, con la pequeña precaución de que los signos &, < y > deben escrbirse como &, < y &gt respectivamente.

El primer paso es informar a calibre de que este documento contiene fórmulas. Para ello se añade el siguiente fragmente do código a la sección <head> del archivo HTML:

<script type="text/x-mathjax-config"></script>

Eso es todo, ahora ya se pueden escribir formulas igual que se haría en un archivo .tex. Por ejemplo, éstas son las ecuaciones de Lorentz:

<h2>The Lorenz Equations</h2>

<p>
\begin{align}
\dot{x} &amp; = \sigma(y-x) \\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align}
</p>

Este fragmento aparece como la siguiente captura de pantalla en el visor de calibre.

Las ecuaciones de Lorentz

El archivo HTML completo, con más ecuaciones y fórmulas en el texto se reproduce más abajo. Puede convertir este archivo HTML a EPUB con calibre para obtener un libro electrónico fácilmente distribuible a otras personas.

<!DOCTYPE html>
<html>
<!-- Copyright (c) 2012 Design Science, Inc. -->
<head>
<title>Math Test Page</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />

<!-- This script tag is needed to make calibre's ebook-viewer recpgnize that this file needs math typesetting -->
<script type="text/x-mathjax-config">
    // This line adds numbers to all equations automatically, unless explicitly suppressed.
    MathJax.tex = {tags: 'all'};
</script>

<style>
h1 {text-align:center}
h2 {
  font-weight: bold;
  background-color: #DDDDDD;
  padding: .2em .5em;
  margin-top: 1.5em;
  border-top: 3px solid #666666;
  border-bottom: 2px solid #999999;
}
</style>
</head>
<body>

<h1>Sample Equations</h1>

<h2>The Lorenz Equations</h2>

<p>
\begin{align}
\dot{x} &amp; = \sigma(y-x) \label{lorenz}\\
\dot{y} &amp; = \rho x - y - xz \\
\dot{z} &amp; = -\beta z + xy
\end{align}
</p>

<h2>The Cauchy-Schwarz Inequality</h2>

<p>\[
\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
 \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
\]</p>

<h2>A Cross Product Formula</h2>

<p>\[
  \mathbf{V}_1 \times \mathbf{V}_2 =
   \begin{vmatrix}
    \mathbf{i} &amp; \mathbf{j} &amp; \mathbf{k} \\
    \frac{\partial X}{\partial u} &amp; \frac{\partial Y}{\partial u} &amp; 0 \\
    \frac{\partial X}{\partial v} &amp; \frac{\partial Y}{\partial v} &amp; 0 \\
   \end{vmatrix}
\]</p>

<h2>The probability of getting \(k\) heads when flipping \(n\) coins is:</h2>

<p>\[P(E) = {n \choose k} p^k (1-p)^{ n-k} \]</p>

<h2>An Identity of Ramanujan</h2>

<p>\[
   \frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
\]</p>

<h2>A Rogers-Ramanujan Identity</h2>

<p>\[
  1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
     \quad\quad \text{for $|q|&lt;1$}.
\]</p>

<h2>Maxwell's Equations</h2>

<p>
\begin{align}
  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &amp; = \frac{4\pi}{c}\vec{\mathbf{j}} \\
  \nabla \cdot \vec{\mathbf{E}} &amp; = 4 \pi \rho \\
  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} &amp; = \vec{\mathbf{0}} \\
  \nabla \cdot \vec{\mathbf{B}} &amp; = 0
\end{align}
</p>

<h2>In-line Mathematics</h2>

<p>While display equations look good for a page of samples, the
ability to mix math and text in a paragraph is also important.  This
expression \(\sqrt{3x-1}+(1+x)^2\) is an example of an inline equation.  As
you see, equations can be used this way as well, without unduly
disturbing the spacing between lines.</p>

<h2>References to equations</h2>

<p>Here is a reference to the Lorenz Equations (\ref{lorenz}). Clicking on the equation number will take you back to the equation.</p>

</body>
</html>

Más información

Puesto que el visor de libros de calibre usa la biblioteca MathJax para mostrar las fórmulas, el mejor lugar para encontrar más información sobre fórmulas matemáticas en libros electrónicos y obtener ayuda es la página web de MathJax [https://www.mathjax.org].

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Crear catálogos AZW3 • EPUB • MOBI

Crear catálogos AZW3 • EPUB • MOBI

La función «Crear catálogo» de calibre le permite crear un catálogo de su biblioteca en diferentes formatos. Este archivo de ayuda describe las opciones para generar catálogos en los formatos AZW3, EPUB y MOBI.

Seleccionar libros para catalogar

Secciones incluidas

Prefijos

Libros excluidos

Géneros excluidos

Otras opciones

Portadas personalizadas para catálogos

Otras fuentes de ayuda

Seleccionar libros para catalogar

Si quiere catalogar todos los libros de su biblioteca, asegúrese de no tener ningún criterio de búsqueda ni filtro seleccionados en la ventana principal. Si tiene un sólo libro seleccionado, todos los libros de la biblioteca serán considerados para el catálogo que genere. Puede excluir libros individuales según distintos criterios; véase la sección Géneros excluidos abajo para más información.

Si desea catalogar sólo algunos libros de la biblioteca, tiene dos opciones:

Cree una selección múltiple de los libros que desea catalogar. Si ha seleccionado más de un libro en la ventana principal de calibre, sólo los libros seleccionados se catalogarán.

Use el campo de Búsqueda o el de Explorador de etiquetas para filtrar los libros que deberán mostrarse. Sólo se catalogarán los libros que se muestran en la ventana principal.

Para comenzar la generación del catálogo, seleccione en el menú Convertir libros > Crear un catálogo con los libros en su la biblioteca calibre. También puede añadir un botón Crear catálogo a la barra de herramientas en Preferencias > Interfaz > Barras de herramientas y menús para facilitar el acceso al diálogo Generar catálogos.

En Opciones del catálogo, seleccione AZW3, EPUB o MOBI como formato del catálogo. En el campo Título del catálogo escriba un nombre para el catálogo que va a generar. Si ya existe un catálogo con el mismo nombre y formato, éste será reemplazado por el nuevo catálogo generado.

Si se activa Enviar el catálogo automáticamente al dispositivo, el catálogo que se genere se descargará en el dispositivo conectado una vez creado.

Secciones incluidas

Las secciones con la casilla marcada se incluirán en el catálogo generado:

Autores — todos los libros, ordenados por autor, se presentan en una lista. Los libros sin serie aparecerán primero, seguidos por los libros con serie.

Títulos — todos los libros, ordenados por título, aparecen en una lista.

Series — todos los libros que pertenecen a una serie, ordenados por serie, aparecen en una lista.

Géneros — géneros individuales se presentan en una lista, ordenados por autor y serie.

Añadidos recientemente — todos los libros, por orden cronológico inverso. La lista incluye los libros que fueron añadidos en los últimos 30 días, seguidos por un listado de libros añadidos mes a mes.

Descripciones — página con una descripción detallada de cada libro, incluyendo una imagen miniatura de la portada y comentarios. Ordenados por autor, con los libros sin serie, seguidos por los libros con serie.

Prefijos

Las reglas de prefijo permiten añadir un prefijo a las listas de libros cuando cumplan ciertos criterios. Por ejemplo, puede marcar los libros leídos ó los libros en su lista de deseos con una X.

La casilla en la primera columna activa la regla. Nombre es el nombre de la regla que usted asigne. Campo puede ser Etiquetas ó una columna personalizada de su biblioteca. Valor es el contenido que Campo debe coincidir. Cuando se cumpla la regla que se indica por un prefijo, el libro tendra el Prefijo seleccionado.

El el ejemplo anterior se especificaron tres reglas de prefijo:

Libro leído especifica que el libro con una fecha cualquiera dentro de la columna personalizada, llamada Leído por última vez tendrá un prefijo señalado con una marca de verificación.

El artículo Deseado indica que aquellos libros con la etiqueta Deseado serán precedidos por una cruz.

Biblioteca especifica que cualquier libro con valor «Verdadero» (o Sí) en la columna personalizada Disponible en la biblioteca estará precedido por una flecha doble.

El prefijo aparece cuando la regla se cumple. Las reglas desactivadas o incompletas se ignoran.

Libros excluidos

Las reglas de Exclusión le permiten indicar los libros que no se incluirán en el catálogo.

La casilla en la primera columna activa la regla. Nombre es el nombre de la regla que usted asigne. Campo puede ser Etiquetas o una columna personalizada de la biblioteca. Valor es el contenido de Campo que debe coincidir. Cuando una regla de exclusión se cumpla, el libro se excluirá del catálogo que se genere.

En el ejemplo anterior se han especificado dos reglas de exclusión:

La regla Catálogos indica que el libro con la etiqueta Catálogo se excluirá del catálogo que genere.

La regla Libros archivados especifica que cualquier libro con un valor de Archivado en la columna personalizada Estado será excluido del catálogo generado.

Todas las reglas se evalúan para cada libro. Las reglas desactivadas o incompletas se ignoran.

Géneros excluidos

Cuando el catálogo se genera, las etiquetas en su base de datos se usan como géneros. Por ejemplo, si usted usa las etiquetas «Ficción» y «No ficción». Estas etiquetas aparecerán como géneros en el catálogo, con los libros en las listas de género según las etiquetas asignadas. Cada libro aparecerá en la lista de los géneros para los que tenga la correspondiente etiqueta.

Puede usar algunas de las etiquetas para diferentes propósitos, por ejemplo, un + puede indicar libro leído ó la etiqueta entre corchetes como «[Promoción de Amazon]» para indicar el origen de un libro. La expresión regular :gilabel:`Géneros excluidos` le permite especificar las etiquetas que no desee usar como género en el catálogo generado. La expresión predeterminada «[.+]|+» se emplea para que las etiquetas de tipo «[etiqueta]», «+», así como la etiqueta predeterminada para los libros leídos, se excluyan de ser empleadas en la lista de géneros del catálogo generado.

También puede usar un nombre de etiqueta exacto en una expresión regular. Por ejemplo, «[Promoción de Amazon]» o «[Proyecto Gutenberg]». Si quiere crear una lista de etiquetas exactas que deben excluirse, ponga un guión (barra vertical) entre ellas: «[Promoción de Amazon]|[Proyecto Gutenberg]».

Resultado de expresión regular muestra la etiquetas a excluir cuando se genere el catálogo, tomando en cuenta, las etiquetas en su base de datos y a la expresión regular que usted seleccionó. El resultado se actualiza con forme usted modifica la expresión regular.

Otras opciones

Portada del catálogo indica si se deberá generar una nueva portada o si se usará una portada existente. Es posible crear una portada personalizada para los catálogos, véase Portadas personalizadas para catálogos para más información. Si ha creado una portada personalizada que quiere volver a usar, seleccione Utilizar portada actual. En caso contrario seleccione Generar nueva portada.

Nota de descripción extra indica el contenido de la columna personalizada que se incluirá en la página de descripción, junto a la imagen miniatura de portada. Por ejemplo, puede mostrar la fecha de la última vez que leyó un libro al usar la columna personalizada Leído por última vez. Para un uso avanzado de la opción de nota de descripción, véase este mensaje en el foro de calibre [https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395].

Ancho de la miniatura especifica la anchura de la imágenes miniaturas de portada que aparecen en las páginas de descripción. Las miniaturas se conservan en caché. Para probar distintos tamaños, intente generar un catálogo con algunos libros hasta que encuentre la anchura que mejor le parece, después genere el catálogo completo. La primera vez que genere un catálogo con una nueva anchura de miniaturas, el proceso será lento, pero después mejorará ya que las imágenes miniaturas se tomarán del caché.

Unir con comentarios especifica que al combinar el contenido del campo de una columna personalizada con los metadatos de comentarios, la información no se alterará cuando se cree el catálogo. Por ejemplo, usted tiene una columna personalizada de nombre Biografía del autor que desea agregar a los comentarios. Puede elegir insertar el contenido de la columna personalizada antes o después de la sección de comentarios y si lo desea, puede separarlos con un guión. Los tipos de columna personalizada que se pueden seleccionar son «texto, comentarios y compuesta».

Portadas personalizadas para catálogos

copia Con el complemento Generate Cover [https://www.mobileread.com/forums/showthread.php?t=124219] instalado, puede crear portadas personalizadas para el catálogo. Para instalar el complemento, vaya a Preferencias > Avanzado > Complementos > Obtener nuevos complementos.

Otras fuentes de ayuda

Para más información sobre las funciones de catálogo de calibre, véase el foro de MobileRead sobre Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556] , donde encontrará información sobre cómo personalizar las plantillas de catálogo y cómo enviar reportes sobre errores.

Para hacer preguntas o discutir sobre las funciones del catálogo calibre con otros usuarios, visite el foro de nombre MobileRead Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236].

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Cursillos »

Bibliotecas virtuales

Bibliotecas virtuales

En calibre, una biblioteca virtual es una manera de hacer que calibre abra sólo un subconjunto de una biblioteca normal. Por ejemplo, es posible que desee trabajar únicamente con los libros de un determinado autor, o sólo con libros que tengan una cierta etiqueta. El uso de las bibliotecas virtuales es la mejor forma de dividir una colección de libros grande en subcolecciones más pequeñas. Es mejor que dividir la biblioteca en múltiples bibliotecas más pequeñas, pues cuando quiera buscar en toda la biblioteca puede simplemente volver a la biblioteca completa. No hay manera de buscar a través de múltiples bibliotecas separadas simultáneamente en calibre.

Una biblioteca virtual es diferente a una simple búsqueda. Una búsqueda sólo restringirá la lista de los libros que aparecen en la lista de libros. Una biblioteca virtual hace eso, y además también restringe las entradas incluidas en Explorador de etiquetas a la izquierda. El explorador de etiquetas sólo mostrará las etiquetas, autores, series, editores, etc, que provienen de los libros en la biblioteca virtual. Por tanto, una biblioteca virtual se comporta como la biblioteca real, pero contiene un conjunto restringido de libros.

Crear bibliotecas virtuales

creación de bibliotecas virtuales Para utilizar una biblioteca virtual, pulse en el botón Biblioteca virtual situado a la izquierda de la barra de búsqueda y seleccione la opción Crear biblioteca virtual. Como primer ejemplo, vamos a crear una biblioteca virtual que nos muestre sólo los libros de un autor concreto. Pulse en el enlace Autores como se muestra en la imagen de abajo, elija el autor que desee utilizar y pulse en Aceptar.

El cuadro de diálogo Crear biblioteca virtual ha sido rellenado automáticamente. Pulse en Aceptar y verá que se ha creado y activado una nueva biblioteca virtual, que muestra únicamente los libros del autor seleccionado. En lo que se refiere a calibre, es como si la biblioteca contuviera sólo los libros del autor seleccionado.

Puede volver a la biblioteca completa en cualquier momento, pulsando en Biblioteca virtual y seleccionando la entrada denominada :guilabel:` <Ninguna>`.

Las bibliotecas virtuales están basadas en búsquedas. Puede utilizar cualquier búsqueda como base de una biblioteca virtual. La biblioteca virtual contendrá sólo los libros que coincidan con esa búsqueda. En primer lugar, escriba la búsqueda que desee utilizar en la barra de búsqueda o construya una búsqueda utilizando el Explorador de etiquetas. Cuando esté satisfecho con los resultados, pulse en el botón Biblioteca virtual, seleccione Crear biblioteca virtual y escriba un nombre para la nueva biblioteca virtual. La biblioteca virtual se creará basada en la búsqueda que acaba de introducir. Estas búsquedas son muy potentes, para más ejemplos de los tipos de cosas que se pueden conseguir, vea La interfaz de búsqueda.

Ejemplos de bibliotecas virtuales útiles

Libros añadidos a calibre en el último día:

date:>1díasatrás

Libros añadidos a calibre el último mes:

date:>30díasatrás

Libros con una calificación de 5 estrellas:

rating:5

Libros con una calificación de al menos 4 estrellas:

rating:>=4

Libros sin calificación:

rating:false

Periódicos descargados por la función Obtener noticias en calibre:

tags:=Noticias y author:=calibre

Libros sin etiquetas:

tags:false

Libros sin portadas:

cover:false

Trabajar con bibliotecas virtuales

Puede modificar una biblioteca virtual ya creada o eliminarla, pulsando en Biblioteca virtual y eligiendo la acción apropiada.

Puede indicar a calibre que siempre quiere aplicar cierta biblioteca virtual al abrir la biblioteca actual, en Preferencias > Interfaz > Comportamiento.

Puede utilizar rápidamente la búsqueda actual como una biblioteca virtual temporal pulsando en el botón Biblioteca virtual y eligiendo la entrada *búsqueda actual.

Puede mostrar todas las bibliotecas virtuales disponibles como pestañas en la parte superior de la lista de libros. Esto es particularmente útil si quiere intercambiar entre las bibliotecas virtuales muy a menudo. Pulse en el botón Biblioteca virtual y seleccione Mostrar bibliotecas virtuales como pestañas. Puede reorganizar las pestañas simplemente arrastrando y soltando, y cerrando aquellas que no desee ver. Las pestañas cerradas se pueden restaurar pulsando con el botón derecho sobre la barra de pestañas.

Usar bibliotecas virtuales en búsquedas

Puede buscar libros que estén en una biblioteca virtual usando el prefijo vl: (por «virtual library»). Por ejemplo vl:Leídos encontrará todos los libros en la biblioteca virtual Leídos. La búsqueda vl:Leídos y vl:"Ciencia ficción" encontrará todos los libros que estén en ambas bibliotecas virtuales Leídos y Ciencia ficción.

El valor despué de vl: debe ser el nombre de una biblioteca virtual. Si el nombre de la biblioteca virtual contiene espacios, póngalo entre comillas.

Un uso para la búsqueda en bibliotecas virtuales es en el servidor de contenidos. En Preferencias > Compartir > Compartir por la red > Requerir nombre de usuario y contraseña puede limitar las bibliotecas de calibre visibles para un usuario. Para cada biblioteca visible puede especificar una expresión de búsqueda para aplicar limitaciones adicionales a los libros visibles. Use vl:"Nombre de biblioteca virtual" para limitar los libros a los pertenecientes a una biblioteca virtual.

Usar restricciones adicionales

Puede restringir aún más los libros que se muestran en una biblioteca virtual usando Restricciones adicionales. Una restricción adicional es una búsqueda guardada creada previamente y que puede aplicarse a la biblioteca virtual actual para restringir los libros mostrados. Por ejemplo, si ya tiene una biblioteca virtual para los libros etiquetados como Ficción histórica y una búsqueda guardada que muestra los libros no leídos, puede pulsar en el botón Biblioteca virtual y elegir la opción Restricción adicional para mostrar sólo los libros de ficción histórica no leídos. Para obtener información sobre búsquedas guardadas, vea Guardar búsquedas.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

El esquema de URL calibre://

El esquema de URL calibre://

calibre se registra como el programa encargado de los URL calibre://. Por lo tanto, podrá usarlos para realizar operaciones como abrir libros, buscar libros, etc. desde otros programas o documentos o mediante la línea de órdenes. Por ejemplo, si ejecuta lo siguiente en la línea de órdenes:

calibre calibre://switch-library/Some_Library

calibre se abrirá con la biblioteca llamada Some Library. Los nombres de biblioteca son el nombre de la carpeta correspondiente, con los espacios sustituidos por guiones bajos. El valor especial _ significa la biblioteca actual. Los distintos tipos de URL se documentan a continuación.

También puede poner estos enlaces en archivos HTML, documentos de Word u otros parecidos y el sistema operativo ejecutará calibre automáticamente para realizar la acción especificada.

Cambiar a una biblioteca específica

Mostrar un libro específico en calibre

Abrir un libro específico en el visor de libros electrónicos en una posición específica

Buscar libros

Abrir la ventana de detalles de un libro en alguna biblioteca

Abrir las notas asociadas con un autor, serie, etc.

Codificación hexadecimal de parámetros de URL

Cambiar a una biblioteca específica

La sintaxis de URL es:

calibre://switch-library/Library_Name

Library_Name es el nombre de la carpeta de la biblioteca, con los espacios sustituidos por guiones bajos. El valor especial _ significa la biblioteca actual. También puede usar la codificación hexadecimal para los nombres de biblioteca, lo que es útil si los nombres de biblioteca tienen caracteres especiales que requerirían usar codificación de URL. Los nombres de biblioteca con codificación hexadecimal son algo como:

_hex_-AD23F4BC

Donde la parte tras el prefijo _hex_- es el nombre de biblioteca codificado como UTF-8 y cada byte está representado por dos caracteres hexadecimales.

Mostrar un libro específico en calibre

La sintaxis de URL es:

calibre://show-book/Library_Name/book_id

Esto mostrará el libro con identificador book_id (un número) en calibre. Los identificadores de los libros pueden verse en la interfaz de calibre pasando el cursor sobre el enlace Pulsar para abrir en el panel de detalles del libro, es el número entre paréntesis al final de la ruta a la carpeta del libro.

Puede copiar un enlace al libro que se muestra actualmente en calibre pulsando con el botón derecho sobre el panel de detalles del libro y eligiendo Copiar enlace al libro.

Si una búsqueda está activa y el libro no coincide con la búsqueda, la búsqueda se borra.

Si hay una biblioteca virtual seleccionada, calibre la usará al mostrar el libro. Si el libro no se encuentra en la biblioteca virtual, se desactiva la biblioteca virtual.

Si quiere cambiar a una biblioteca virtual concreta al mostrar el libro, use:

calibre://show-book/Library_Name/book_id?virtual_library=Library%20Name
or
calibre://show-book/Library_Name/book_id?encoded_virtual_library=hex_encoded_virtual_library_name

sustituyendo los espacios en el nombre de la biblioteca virtual por %20. Si el libro no se encuentra en esa biblioteca virtual, se ignora la biblioteca virtual.

Abrir un libro específico en el visor de libros electrónicos en una posición específica

La sintaxis de URL es:

calibre://view-book/Library_Name/book_id/book_format?open_at=location

Aquí, book_format es el formato del libro, por ejemplo, EPUB o MOBI, y location es una ubicación opcional dentro del libro. La manera más sencilla de obtener estos enlaces es abrir el libro en el visor, seleccionar Ir a > Ubicación en los controles del visor, y allí se mostrará el enlace que podrá copiar y pegar en otra parte.

Buscar libros

La sintaxis de URL es:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

Aquí query es cualquier expresión de búsqueda válida. Si la expresión de búsqueda es complicada, use eq y la codificación hexadecimal. Si se omite la consulta, se limpiará la búsqueda actual.

De manera predeterminada, si hay una biblioteca virtual seleccionada, calibre la desactivará antes de realizar la búsqueda para asegurarse de que se encuentran todos los libros. SI desea mantener la biblioteca virtual, use:

calibre://search/Library_Name?q=query&virtual_library=_

Si quiere cambiar a una biblioteca virtual concreta, use:

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

sustituyendo los espacios en el nombre de la biblioteca virtual por %20.

Si realiza una búsqueda en calibre y quiere generar un enlace para ella, puede hacerlo pulsando con el botón derecho sobre la barra de búsqueda y eligiendo Copiar búsqueda como URL.

Abrir la ventana de detalles de un libro en alguna biblioteca

La sintaxis de URL es:

calibre://book-details/Library_Name/book_id

Esto abre la ventana de detalles del libro para el libro especificado de la biblioteca especificada, sin cambiar la biblioteca actual o el libro seleccionado.

Abrir las notas asociadas con un autor, serie, etc.

La sintaxis de URL es:

calibre://book-details/Library_Name/Field_Name/id_Item_Id

Esto abre una ventana que muestra las notas del elemento especificado. La manera más sencilla de crear un URL así es mostrar las notas que desee en calibre y pulsar el botón Copiar URL para copiar el URL al portapapeles y pegarlo donde lo necesite.

Aquí Field_Name es el nombre de las columnas, como authors o etiquetas. Para columnas creadas por el usuario sustituya el # inicial en el nombre de campo por un guión bajo, de manera que #misetiquetas se hace _misetiquetas.

Además de especificar elementos por id usando Item_Id, también puede especificarlos por nombre usando bien val_Item_Name o hex_Hex_Encoded_Item_Name. Por ejemplo:

calibre://book-details/Library_Name/authors/val_John%20Doe

Codificación hexadecimal de parámetros de URL

La codificación hexadecimal de los parámetros de URL se hace codificando en primer lugar el parámetro como bytes en UTF-8, y luego sustituyendo cada byte por dos caracteres hexadecimales que lo representan. Por ejemplo, el texto abc corresponde a los bytes 0x61 0x62 0x63 en UTF-8, así que la versión codificada del texto será 616263.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Personalizar calibre

Personalizar calibre

calibre tiene un diseño altamente modular. Muchas partes pueden personalizarse. Aquí aprenderá:

cómo usar variables de entorno y ajustes para personalizar el comportamiento de calibre

cómo especificar recursos estáticos propios, como iconos y plantillas, para sustituir los predeterminados

cómo usar complementos para añadir funcionalidades a calibre

cómo compartir temas de iconos y complementos con otros usuarios de calibre

para ver cómo crear fórmulas para añadir nuevos orígenes de contenido en línea a calibre, visite la sección Añadir su sitio de noticias favorito.

Nota

Tenga en cuenta que aunque los temas de iconos y complementos pueden verse y descargarse en el actualizador incorporado de calibre, no son parte de calibre, y el lugar primario para obtener soporte y código fuente en en los foros de Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] en sus respectivos hilos de soporte.

Variables de entorno

Ajustes

Reemplazar iconos, plantillas, etcétera

Crear un tema de iconos propio para calibre

Personalizar calibre con complementos

Variables de entorno

CALIBRE_CONFIG_DIRECTORY - establece la carpeta donde se guardan y de donde se leen los archivos de configuración

CALIBRE_TEMP_DIR - establece la carpeta temporal utilizado por calibre

CALIBRE_CACHE_DIRECTORY - establece la carpeta que calibre usa para almacenar datos temporales persistentes entre sesiones.

CALIBRE_OVERRIDE_DATABASE_PATH - permite especificar la ruta completa a metadata.db. Usando esta variable metadata.db puede tener metadata.db en una ubicación distinta de la carpeta de la biblioteca. Esto es útil si la carpeta de la biblioteca está en una unidad de red que no admite el bloqueo de archivos.

CALIBRE_DEVELOP_FROM - se utiliza para ejecutar desde un entorno de desarrollo de calibre. Vea Configurar un entorno de desarrollo de calibre.

CALIBRE_OVERRIDE_LANG - se utiliza para forzar el idioma de la interfaz (código de idioma ISO 639)

CALIBRE_TEST_TRANSLATION - se utiliza para probar un archivo .po de traducción (debe ser la ruta al archivo .po)

CALIBRE_NO_NATIVE_FILEDIALOGS - hace que calibre no utilice los cuadros de diálogo nativos para la selección de archivos y carpetas.

CALIBRE_NO_NATIVE_MENUBAR - hace que calibre no cree un menú nativo (global) en Ubuntu Unity y otros entornos de escritorio de Linux. El menú se crea dentro de la ventana, que es más tradicional.

CALIBRE_USE_SYSTEM_THEME - de manera predeterminada, en Linux, calibre usa su propio estilo Qt incorporado. Esto es así para evitar problemas causados por incompatibilidades entre la versión de Qt con la que está construido calibre y la del sistema. La desventaja es que calibre puede no adecuarse a la apariencia del sistema. Si establece esta variable de entorno en Linux, hará que calibre use el tema del sistema; sea consciente de que puede haber fallos y cuelgues.

CALIBRE_SHOW_DEPRECATION_WARNINGS - hace que calibre envíe advertencias de elementos anticuados a stdout. Útil para desarrolladores de calibre.

CALIBRE_NO_DEFAULT_PROGRAMS - evita que calibre registre automáticamente los tipos de archivo que reconoce en Windows.

QT_QPA_PLATFORM – En Linux, asigne el valor wayland para hacer que calibre use Wayland y xcb para que use X11.

SYSFS_PATH - Úsela si sysfs está montado en una ubicación distinta de /sys

http_proxy, https_proxy - utilizadas en Linux para especificar un proxy HTTP(S)

Véase How to set environment variables in Windows [https://www.computerhope.com/issues/ch000549.htm] (en inglés). Si está en macOS puede establecer variables de entorno creando el archivo ~/Library/Preferences/calibre/macos-env.txt y poniendo en él una variable por línea, por ejemplo:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Ajustes

Los ajustes son pequeños cambios que puede especificar para controlar varios aspectos del comportamiento de calibre. Puede cambiarlos en Preferencias > Avanzado > Ajustes. Los valores predeterminados para los ajustes se muestran a continuación

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
# License: GPLv3 Copyright: 2010, Kovid Goyal <kovid at kovidgoyal.net>

# Contains various tweaks that affect calibre behavior. Only edit this file if
# you know what you are doing. If you delete this file, it will be recreated from
# defaults.

#: Auto increment series index
# The algorithm used to assign a book added to an existing series a series number.
# New series numbers assigned using this tweak are always integer values, except
# if a constant non-integer is specified.
# Possible values are:
#   next - First available integer larger than the largest existing number
#   first_free - First available integer larger than 0
#   next_free - First available integer larger than the smallest existing number
#   last_free - First available integer smaller than the largest existing number. Return largest existing + 1 if no free number is found
#   const - Assign the number 1 always
#   no_change - Do not change the series index
#   a number - Assign that number always. The number is not in quotes. Note that 0.0 can be used here.
# Examples:
#   series_index_auto_increment = 'next'
#   series_index_auto_increment = 'next_free'
#   series_index_auto_increment = 16.5
#
# Set the use_series_auto_increment_tweak_when_importing tweak to True to
# use the above values when importing/adding books. If this tweak is set to
# False (the default) then the series number will be set to 1 if it is not
# explicitly set during the import. If set to True, then the
# series index will be set according to the series_index_auto_increment setting.
# Note that the use_series_auto_increment_tweak_when_importing tweak is used
# only when a value is not provided during import. If the importing regular
# expression produces a value for series_index, or if you are reading metadata
# from books and the import plugin produces a value, then that value will
# be used irrespective of the setting of the tweak.
series_index_auto_increment = 'next'
use_series_auto_increment_tweak_when_importing = False

#: Add separator after completing an author name
# Set this if the completion separator should be appended to the end of the
# completed text to automatically begin a new completion operation for authors.
# It can be either True or False
authors_completer_append_separator = False

#: Author sort name algorithm
# The algorithm used to copy author to author_sort.
# Possible values are:
#  invert: use "fn ln" -> "ln, fn"
#  copy  : copy author to author_sort without modification
#  comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
#  nocomma : "fn ln" -> "ln fn" (without the comma)
# When this tweak is changed, the author_sort values stored with each author
# must be recomputed by right-clicking on an author in the left-hand tags
# panel, selecting 'Manage authors', and pressing
# 'Recalculate all author sort values'.
#
# The author_name_suffixes are words that are ignored when they occur at the
# end of an author name. The case of the suffix is ignored and trailing
# periods are automatically handled.
#
# The same is true for author_name_prefixes.
#
# The author_name_copywords are a set of words which, if they occur in an
# author name, cause the automatically generated author sort string to be
# identical to the author's name. This means that the sort for a string like
# "Acme Inc." will be "Acme Inc." instead of "Inc., Acme".
#
# If author_use_surname_prefixes is enabled, any of the words in
# author_surname_prefixes will be treated as a prefix to the surname, if they
# occur before the surname. So for example, "John von Neumann" would be sorted
# as "von Neumann, John" and not "Neumann, John von".
author_sort_copy_method = 'comma'
author_name_suffixes = ('Jr', 'Sr', 'Inc', 'Ph.D', 'Phd',
                        'MD', 'M.D', 'I', 'II', 'III', 'IV',
                        'Junior', 'Senior')
author_name_prefixes = ('Mr', 'Mrs', 'Ms', 'Dr', 'Prof')
author_name_copywords = (
    'Agency', 'Corporation', 'Company', 'Co.', 'Council',
    'Committee', 'Inc.', 'Institute', 'National', 'Society', 'Club', 'Team',
    'Software', 'Games', 'Entertainment', 'Media', 'Studios',
)
author_use_surname_prefixes = False
author_surname_prefixes = ('da', 'de', 'di', 'la', 'le', 'van', 'von')

#: Splitting multiple author names
# By default, calibre splits a string containing multiple author names on
# ampersands and the words "and" and "with". You can customize the splitting
# by changing the regular expression below. Strings are split on whatever the
# specified regular expression matches, in addition to ampersands.
# Default: r'(?i),?\s+(and|with)\s+'
authors_split_regex = r'(?i),?\s+(and|with)\s+'

#: Use author sort in Tag browser
# Set which author field to display in the Tag browser (the list of authors,
# series, publishers etc on the left hand side). The choices are author and
# author_sort. This tweak affects only what is displayed under the authors
# category in the Tag browser and Content server. Please note that if you set this
# to author_sort, it is very possible to see duplicate names in the list because
# although it is guaranteed that author names are unique, there is no such
# guarantee for author_sort values. Showing duplicates won't break anything, but
# it could lead to some confusion. When using 'author_sort', the tooltip will
# show the author's name.
# Examples:
#   categories_use_field_for_author_name = 'author'
#   categories_use_field_for_author_name = 'author_sort'
categories_use_field_for_author_name = 'author'

#: Control partitioning of Tag browser
# When partitioning the Tag browser, the format of the subcategory label is
# controlled by a template: categories_collapsed_name_template if sorting by
# name, categories_collapsed_rating_template if sorting by average rating, and
# categories_collapsed_popularity_template if sorting by popularity. There are
# two variables available to the template: first and last. The variable 'first'
# is the initial item in the subcategory, and the variable 'last' is the final
# item in the subcategory. Both variables are 'objects'; they each have multiple
# values that are obtained by using a suffix. For example, first.name for an
# author category will be the name of the author. The sub-values available are:
#  name: the printable name of the item
#  count: the number of books that references this item
#  avg_rating: the average rating of all the books referencing this item
#  sort: the sort value. For authors, this is the author_sort for that author
#  category: the category (e.g., authors, series) that the item is in.
# Note that the "r'" in front of the { is necessary if there are backslashes
# (\ characters) in the template. It doesn't hurt anything to leave it there
# even if there aren't any backslashes.
categories_collapsed_name_template = r'{first.sort:shorten(4,,0)} - {last.sort:shorten(4,,0)}'
categories_collapsed_rating_template = r'{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}'
categories_collapsed_popularity_template = r'{first.count:d} - {last.count:d}'

#: Specify columns to sort the booklist by on startup
# Provide a set of columns to be sorted on when calibre starts.
# The argument is None if saved sort history is to be used
# otherwise it is a list of column,order pairs. Column is the
# lookup/search name, found using the tooltip for the column
# Order is 0 for ascending, 1 for descending.
# For example, set it to [('authors',0),('title',0)] to sort by
# title within authors.
sort_columns_at_startup = None

#: Control how dates are displayed
# Format to be used for publication date and the timestamp (date).
#  A string controlling how the publication date is displayed in the GUI
#  d     the day as number without a leading zero (1 to 31)
#  dd    the day as number with a leading zero (01 to 31)
#  ddd   the abbreviated localized day name (e.g. 'Mon' to 'Sun').
#  dddd  the long localized day name (e.g. 'Monday' to 'Sunday').
#  M     the month as number without a leading zero (1-12)
#  MM    the month as number with a leading zero (01-12)
#  MMM   the abbreviated localized month name (e.g. 'Jan' to 'Dec').
#  MMMM  the long localized month name (e.g. 'January' to 'December').
#  yy    the year as two digit number (00-99)
#  yyyy  the year as four digit number
#  h     the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm) '
#  hh    the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm) '
#  m     the minutes without a leading 0 (0 to 59) '
#  mm    the minutes with a leading 0 (00 to 59) '
#  s     the seconds without a leading 0 (0 to 59) '
#  ss    the seconds with a leading 0 (00 to 59) '
#  ap    use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the localized string for am or pm
#  AP    use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the localized string for AM or PM
#  iso   the date with time and timezone. Must be the only format present
#  For example, given the date of 9 Jan 2010, the following formats show
#  MMM yyyy ==> Jan 2010    yyyy ==> 2010       dd MMM yyyy ==> 09 Jan 2010
#  MM/yyyy ==> 01/2010      d/M/yy ==> 9/1/10   yy ==> 10
#
# publication default if not set: MMM yyyy
# timestamp default if not set: dd MMM yyyy
# last_modified_display_format if not set: dd MMM yyyy
gui_pubdate_display_format = 'MMM yyyy'
gui_timestamp_display_format = 'dd MMM yyyy'
gui_last_modified_display_format = 'dd MMM yyyy'

#: Control sorting of titles and series in the library display
# Control title and series sorting in the library view. If set to
# 'library_order', the title sort field will be used instead of the title.
# Unless you have manually edited the title sort field, leading articles such as
# The and A will be ignored. If set to 'strictly_alphabetic', the titles will be
# sorted as-is (sort by title instead of title sort). For example, with
# library_order, The Client will sort under 'C'. With strictly_alphabetic, the
# book will sort under 'T'.
# This flag affects calibre's library display. It has no effect on devices. In
# addition, titles for books added before changing the flag will retain their
# order until the title is edited. Editing a title and hitting Enter
# without changing anything is sufficient to change the sort. Or you can use
# the 'Update title sort' action in the Bulk metadata edit dialog to update
# it for many books at once.
title_series_sorting = 'library_order'

#: Control formatting of title and series when used in templates
# Control how title and series names are formatted when saving to disk/sending
# to device. The behavior depends on the field being processed. If processing
# title, then if this tweak is set to 'library_order', the title will be
# replaced with title_sort. If it is set to 'strictly_alphabetic', then the
# title will not be changed. If processing series, then if set to
# 'library_order', articles such as 'The' and 'An' will be moved to the end. If
# set to 'strictly_alphabetic', the series will be sent without change.
# For example, if the tweak is set to library_order, "The Lord of the Rings"
# will become "Lord of the Rings, The". If the tweak is set to
# strictly_alphabetic, it would remain "The Lord of the Rings". Note that the
# formatter function raw_field will return the base value for title and
# series regardless of the setting of this tweak.
save_template_title_series_sorting = 'library_order'

#: Set the list of words considered to be "articles" for sort strings
# Set the list of words that are to be considered 'articles' when computing the
# title sort strings. The articles differ by language. By default, calibre uses
# a combination of articles from English and whatever language the calibre user
# interface is set to. In addition, in some contexts where the book language is
# available, the language of the book is used. You can change the list of
# articles for a given language or add a new language by editing
# per_language_title_sort_articles. To tell calibre to use a language other
# than the user interface language, set, default_language_for_title_sort. For
# example, to use German, set it to 'deu'. A value of None means the user
# interface language is used. The setting title_sort_articles is ignored
# (present only for legacy reasons).
per_language_title_sort_articles = {
        # English
        'eng'  : (r'A\s+', r'The\s+', r'An\s+'),
        # Esperanto
        'epo': (r'La\s+', r"L'", 'L´'),
        # Spanish
        'spa'  : (r'El\s+', r'La\s+', r'Lo\s+', r'Los\s+', r'Las\s+', r'Un\s+',
                  r'Una\s+', r'Unos\s+', r'Unas\s+'),
        # French
        'fra'  : (r'Le\s+', r'La\s+', r"L'", u'L´', u'L’', r'Les\s+', r'Un\s+', r'Une\s+',
                  r'Des\s+', r'De\s+La\s+', r'De\s+', r"D'", r'D´', r'D’'),
        # Polish
        'pol': (),
        # Italian
        'ita': ('Lo\\s+', 'Il\\s+', "L'", 'L´', 'La\\s+', 'Gli\\s+',
                'I\\s+', 'Le\\s+', 'Uno\\s+', 'Un\\s+', 'Una\\s+', "Un'",
                'Un´', 'Dei\\s+', 'Degli\\s+', 'Delle\\s+', 'Del\\s+',
                'Della\\s+', 'Dello\\s+', "Dell'", 'Dell´'),
        # Portuguese
        'por'  : (r'A\s+', r'O\s+', r'Os\s+', r'As\s+', r'Um\s+', r'Uns\s+',
                  r'Uma\s+', r'Umas\s+', ),
        # Romanian
        'ron'  : (r'Un\s+', r'O\s+', r'Nişte\s+', ),
        # German
        'deu'  : (r'Der\s+', r'Die\s+', r'Das\s+', r'Den\s+', r'Ein\s+',
                  r'Eine\s+', r'Einen\s+', r'Dem\s+', r'Des\s+', r'Einem\s+',
                  r'Eines\s+'),
        # Dutch
        'nld'  : (r'De\s+', r'Het\s+', r'Een\s+', r"'n\s+", r"'s\s+", r'Ene\s+',
                  r'Ener\s+', r'Enes\s+', r'Den\s+', r'Der\s+', r'Des\s+',
                  r"'t\s+"),
        # Swedish
        'swe'  : (r'En\s+', r'Ett\s+', r'Det\s+', r'Den\s+', r'De\s+', ),
        # Turkish
        'tur'  : (r'Bir\s+', ),
        # Afrikaans
        'afr'  : (r"'n\s+", r'Die\s+', ),
        # Greek
        'ell'  : (r'O\s+', r'I\s+', r'To\s+', r'Ta\s+', r'Tus\s+', r'Tis\s+',
                  r"'Enas\s+", r"'Mia\s+", r"'Ena\s+", r"'Enan\s+", ),
        # Hungarian
        'hun'  : (r'A\s+', r'Az\s+', r'Egy\s+',),
}
default_language_for_title_sort = None
title_sort_articles=r'^(A|The|An)\s+'

#: Specify a folder calibre should connect to at startup
# Specify a folder that calibre should connect to at startup using
# connect_to_folder. This must be a full path to the folder. If the folder does
# not exist when calibre starts, it is ignored.
# Example for Windows:
#     auto_connect_to_folder = 'C:/Users/someone/Desktop/testlib'
# Example for other operating systems:
#     auto_connect_to_folder = '/home/dropbox/My Dropbox/someone/library'
auto_connect_to_folder = ''

#: Specify renaming rules for SONY collections
# Specify renaming rules for SONY collections. This tweak is only applicable if
# metadata management is set to automatic. Collections on SONYs are named
# depending upon whether the field is standard or custom. A collection derived
# from a standard field is named for the value in that field.
#
# For example, if the standard 'series' column contains the value 'Darkover', then the
# collection name is 'Darkover'. A collection derived from a custom field will
# have the name of the field added to the value. For example, if a custom series
# column named 'My Series' contains the name 'Darkover', then the collection
# will by default be named 'Darkover (My Series)'. For purposes of this
# documentation, 'Darkover' is called the value and 'My Series' is called the
# category. If two books have fields that generate the same collection name,
# then both books will be in that collection.
#
# This set of tweaks lets you specify for a standard or custom field how
# the collections are to be named. You can use it to add a description to a
# standard field, for example 'Foo (Tag)' instead of the 'Foo'. You can also use
# it to force multiple fields to end up in the same collection.
#
# For example, you could force the values in 'series', '#my_series_1', and
# '#my_series_2' to appear in collections named 'some_value (Series)', thereby
# merging all of the fields into one set of collections.
#
# There are two related tweaks. The first determines the category name to use
# for a metadata field.  The second is a template, used to determines how the
# value and category are combined to create the collection name.
# The syntax of the first tweak, sony_collection_renaming_rules, is:
# {'field_lookup_name':'category_name_to_use', 'lookup_name':'name', ...}
#
# The second tweak, sony_collection_name_template, is a template. It uses the
# same template language as plugboards and save templates. This tweak controls
# how the value and category are combined together to make the collection name.
# The only two fields available are {category} and {value}. The {value} field is
# never empty. The {category} field can be empty. The default is to put the
# value first, then the category enclosed in parentheses, it isn't empty:
# '{value} {category:|(|)}'
#
# Examples: The first three examples assume that the second tweak
# has not been changed.
#
# 1) I want three series columns to be merged into one set of collections. The
# column lookup names are 'series', '#series_1' and '#series_2'. I want nothing
# in the parenthesis. The value to use in the tweak value would be:
#    sony_collection_renaming_rules={'series':'', '#series_1':'', '#series_2':''}
#
# 2) I want the word '(Series)' to appear on collections made from series, and
# the word '(Tag)' to appear on collections made from tags. Use:
#   sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
#
# 3) I want 'series' and '#myseries' to be merged, and for the collection name
# to have '(Series)' appended. The renaming rule is:
#   sony_collection_renaming_rules={'series':'Series', '#myseries':'Series'}
#
# 4) Same as example 2, but instead of having the category name in parentheses
# and appended to the value, I want it prepended and separated by a colon, such
# as in Series: Darkover. I must change the template used to format the category name
#
# The resulting two tweaks are:
#    sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}
#    sony_collection_name_template='{category:||: }{value}'
sony_collection_renaming_rules={}
sony_collection_name_template='{value}{category:| (|)}'

#: Specify how SONY collections are sorted
# Specify how SONY collections are sorted. This tweak is only applicable if
# metadata management is set to automatic. You can indicate which metadata is to
# be used to sort on a collection-by-collection basis. The format of the tweak
# is a list of metadata fields from which collections are made, followed by the
# name of the metadata field containing the sort value.
# Example: The following indicates that collections built from pubdate and tags
# are to be sorted by the value in the custom column '#mydate', that collections
# built from 'series' are to be sorted by 'series_index', and that all other
# collections are to be sorted by title. If a collection metadata field is not
# named, then if it is a series- based collection it is sorted by series order,
# otherwise it is sorted by title order.
# [(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], 'title')]
# Note that the bracketing and parentheses are required. The syntax is
# [ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]
# Default: empty (no rules), so no collection attributes are named.
sony_collection_sorting_rules = []

#: Control how tags are applied when copying books to another library
# Set this to True to ensure that tags in 'Tags to add when adding
# a book' are added when copying books to another library
add_new_book_tags_when_importing_books = False

#: Set the maximum number of sort 'levels'
# Set the maximum number of sort 'levels' that calibre will use to resort the
# library after certain operations such as searches or device insertion. Each
# sort level adds a performance penalty. If the database is large (thousands of
# books) the penalty might be noticeable. If you are not concerned about multi-
# level sorts, and if you are seeing a slowdown, reduce the value of this tweak.
maximum_resort_levels = 5

#: Choose whether dates are sorted using visible fields
# Date values contain both a date and a time. When sorted, all the fields are
# used, regardless of what is displayed. Set this tweak to True to use only
# the fields that are being displayed.
sort_dates_using_visible_fields = False

#: Fuzz value for trimming covers
# The value used for the fuzz distance when trimming a cover.
# Colors within this distance are considered equal.
# The distance is in absolute intensity units.
cover_trim_fuzz_value = 10

#: Control behavior of the book list
# You can control the behavior of double clicks and pressing Enter on the books
# list. Choices: open_viewer, do_nothing, show_book_details,
# show_locked_book_details, edit_cell, edit_metadata. Selecting anything other
# than open_viewer, show_book_details, or show_locked_book_details has the side
# effect of disabling editing a field using a single click.
# Default: open_viewer.
# Example: doubleclick_on_library_view = 'do_nothing'
# You can also control whether the book list scrolls per item or
# per pixel. Default is per item.
doubleclick_on_library_view = 'open_viewer'
enter_key_behavior = 'do_nothing'
horizontal_scrolling_per_column = False
vertical_scrolling_per_row = False

#: Language to use when sorting
# Setting this tweak will force sorting to use the
# collating order for the specified language. This might be useful if you run
# calibre in English but want sorting to work in the language where you live.
# Set the tweak to the desired ISO 639-1 language code, in lower case.
# You can find the list of supported locales at
# https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
# Default: locale_for_sorting = '' -- use the language calibre displays in
# Example: locale_for_sorting = 'fr' -- sort using French rules.
# Example: locale_for_sorting = 'nb' -- sort using Norwegian rules.
locale_for_sorting =  ''

#: The number of seconds to wait before sending emails
# The number of seconds to wait before sending emails when using a
# public email server like GMX/Hotmail/Gmail. Default is: 5 minutes
# Setting it to lower may cause the server's SPAM controls to kick in,
# making email sending fail. Changes will take effect only after a restart of
# calibre. You can also change the list of hosts that calibre considers
# to be public relays here. Any relay host ending with one of the suffixes
# in the list below will be considered a public email server.
public_smtp_relay_delay = 301
public_smtp_relay_host_suffixes = ['gmail.com', 'live.com', 'gmx.com', 'outlook.com']

#: The maximum width and height for covers saved in the calibre library
# All covers in the calibre library will be resized, preserving aspect ratio,
# to fit within this size. This is to prevent slowdowns caused by extremely
# large covers
maximum_cover_size = (1650, 2200)

#: Where to send downloaded news
# When automatically sending downloaded news to a connected device, calibre
# will by default send it to the main memory. By changing this tweak, you can
# control where it is sent. Valid values are "main", "carda", "cardb". Note
# that if there isn't enough free space available on the location you choose,
# the files will be sent to the location with the most free space.
send_news_to_device_location = "main"

#: Unified toolbar on macOS
# If you enable this option and restart calibre, the toolbar will be 'unified'
# with the titlebar as is normal for macOS applications. However, doing this has
# various bugs, for instance the minimum width of the toolbar becomes twice
# what it should be and it causes other random bugs on some systems, so turn it
# on at your own risk!
unified_title_toolbar_on_osx = False

#: Save original file when converting/polishing from same format to same format
# When calibre does a conversion from the same format to the same format, for
# example, from EPUB to EPUB, the original file is saved, so that in case the
# conversion is poor, you can tweak the settings and run it again. By setting
# this to False you can prevent calibre from saving the original file.
# Similarly, by setting save_original_format_when_polishing to False you can
# prevent calibre from saving the original file when polishing.
save_original_format = True
save_original_format_when_polishing = True

#: Number of recently viewed books to show
# Right-clicking the "View" button shows a list of recently viewed books. Control
# how many should be shown, here.
gui_view_history_size = 15

#: Change the font size of the Book details panel in the interface
# Change the font size at which book details are rendered in the side panel and
# comments are rendered in the metadata edit dialog. Set it to a positive or
# negative number to increase or decrease the font size.
change_book_details_font_size_by = 0

#: What format to default to when using the "Unpack book" feature
# The "Unpack book" feature of calibre allows direct editing of a book format.
# If multiple formats are available, calibre will offer you a choice
# of formats, defaulting to your preferred output format if it is available.
# Set this tweak to a specific value of 'EPUB' or 'AZW3' to always default
# to that format rather than your output format preference.
# Set to a value of 'remember' to use whichever format you chose last time you
# used the "Unpack book" feature.
# Examples:
#   default_tweak_format = None       (Use output format)
#   default_tweak_format = 'EPUB'
#   default_tweak_format = 'remember'
default_tweak_format = None

#: Do not preselect a completion when editing authors/tags/series/etc.
# This means that you can make changes and press Enter and your changes will
# not be overwritten by a matching completion. However, if you wish to use the
# completions you will now have to press Tab to select one before pressing
# Enter. Which technique you prefer will depend on the state of metadata in
# your library and your personal editing style.
#
# If preselect_first_completion is False and you want Tab to accept what you
# typed instead of the first completion then set tab_accepts_uncompleted_text
# to True. If you do this then to select from the completions you must press
# the Down or Up arrow keys. The tweak tab_accepts_uncompleted_text is ignored
# if preselect_first_completion is True
preselect_first_completion = False
tab_accepts_uncompleted_text = False

#: Completion mode when editing authors/tags/series/etc.
# By default, when completing items, calibre will show you all the candidates
# that start with the text you have already typed. You can instead have it show
# all candidates that contain the text you have already typed. To do this, set
# completion_mode to 'contains'. For example, if you type asi it will match both
# Asimov and Quasimodo, whereas the default behavior would match only Asimov.
completion_mode = 'prefix'

#: Sort the list of libraries alphabetically
# The list of libraries in the Copy to library and Quick switch menus are
# normally sorted by most used. However, if there are more than a certain
# number of such libraries, the sorting becomes alphabetic. You can set that
# number here. The default is ten libraries.
many_libraries = 10

#: Choose available output formats for conversion
# Restrict the list of available output formats in the conversion dialogs.
# For example, if you only want to convert to EPUB and AZW3, change this to
# restrict_output_formats = ['EPUB', 'AZW3']. The default value of None causes
# all available output formats to be present.
restrict_output_formats = None

#: Set the thumbnail image quality used by the Content server
# The quality of a thumbnail is largely controlled by the compression quality
# used when creating it. Set this to a larger number to improve the quality.
# Note that the thumbnails get much larger with larger compression quality
# numbers.
# The value can be between 50 and 99
content_server_thumbnail_compression_quality = 75

#: Image file types to treat as e-books when dropping onto the "Book details" panel
# Normally, if you drop any image file in a format known to calibre onto the
# "Book details" panel, it will be used to set the cover. If you want to store
# some image types as e-books instead, you can set this tweak.
# Examples:
#    cover_drop_exclude = {'tiff', 'webp'}
cover_drop_exclude = ()

#: Exclude fields when copy/pasting metadata
# You can ask calibre to not paste some metadata fields when using the
# Edit metadata->Copy metadata/Paste metadata actions. For example,
# exclude_fields_on_paste = ['cover', 'timestamp', '#mycolumn']
# to prevent pasting of the cover, Date and custom column, mycolumn.
# You can also add a shortcut in Preferences->Shortcuts->Edit metadata
# to paste metadata ignoring this tweak.
exclude_fields_on_paste = []

#: Skip internet connected check
# Skip checking whether the internet is available before downloading news.
# Useful if for some reason your operating systems network checking
# facilities are not reliable (for example NetworkManager on Linux).
skip_network_check = False

#: Tab stop width in the template editor
# Sets the width of the tab stop in the template editor in "average characters".
# For example, a value of 1 results in a space with the width of one average character.
template_editor_tab_stop_width = 4

#: Value for undefined numbers when sorting
# Sets the value to use for undefined numbers when sorting.
# For example, the value -10 sorts undefined numbers as if they were set to -10.
# Use 'maximum' for the largest possible number. Use 'minimum' for the smallest
# possible number. Quotes are optional if entering a number.
# Examples:
#   value_for_undefined_numbers_when_sorting = -100
#   value_for_undefined_numbers_when_sorting = '2'
#   value_for_undefined_numbers_when_sorting = -0.01
#   value_for_undefined_numbers_when_sorting = 'minimum'
#   value_for_undefined_numbers_when_sorting = 'maximum'
value_for_undefined_numbers_when_sorting = 0

#: Allow template database functions in composite columns
# If True then the template database functions book_values() and book_count()
# can be used in composite custom columns. Note: setting this tweak to True and
# using these functions in composites can be very slow.
# Default: False
allow_template_database_functions_in_composites = False

#: Change the programs that are run when opening files/URLs
# By default, calibre passes URLs to the operating system to open using
# whatever default programs are configured there. Here you can override
# that by specifying the program to use, per URL type. For local files,
# the type is "file" and for web links it is "http*". For example:
# openers_by_scheme = { "http*": "firefox %u" } will make calibre run Firefox
# for https://whatever URLs. %u is replaced by the URL to be opened. The scheme
# takes a glob pattern allowing a single entry to match multiple URL types.
openers_by_scheme = {}

#: Set the first day of the week for calendar popups
# It must be one of the values Default, Sunday, Monday, Tuesday, Wednesday,
# Thursday, Friday, or Saturday, all in English, spelled exactly as shown.
calendar_start_day_of_week = 'Default'

Reemplazar iconos, plantillas, etcétera

Nota

calibre admite directamente temas de iconos, hay varios temas de iconos disponibles para calibre, que puede usar en Preferencias > Interfaz > Apariencia > Cambiar tema de iconos. Es preferible usar temas de iconos en lugar de sustituir iconos individuales.

calibre le permite reemplazar los recursos estáticos, como iconos, JavaScript y plantillas para la sobrecubierta de metadatos, catálogos, etc., con versiones personalizadas. Todos los recursos estáticos se almacenan en la subcarpeta resources. En Windows, se encuentra por lo general en C:\Archivos de programa\Calibre2\app\resources. En macOS, /Aplicaciones/calibre.app/Contents/Resources/resources/. En Linux, si está usando el instalador binario del sitio web de calibre, será /opt/calibre/resources. Estas rutas de acceso pueden cambiar dependiendo de dónde decida instalar calibre.

No debe modificar los archivos de esta carpeta de recursos, ya que los cambios serán reemplazados la próxima vez que actualice calibre. En lugar de ello, vaya a Preferencias > Avanzado > Miscelánea y pulse Abrir la carpeta de configuración calibre. En esta carpeta de configuración, cree una subcarpeta llamada resources y coloque los archivos que quiere reemplazar. Coloque los archivos en las subcarpetas apropiadas, por ejemplo coloque las imágenes en resources/images, etc. calibre usará automaticamente los archivos personalizados la próxima vez que se inicie.

Por ejemplo, si quisiera cambiar el icono para la acción Eliminar libros, primero debería examinar la carpeta de recursos de calibre y ver que el archivo relevante es resources/images/remove_books.png. Asumiendo que cuenta con un icono alternativo en formato PNG llamado myremove_books.png, lo guardaría en la carpeta de configuración como resources/images/remove_books.png. Todos los iconos usados por la interfaz de usuario de calibre están en resources/images y sus subcarpetas. Los archivos de sustitución colocados aquí tienen mayor prioridad que los temas de iconos personalizados.

Crear un tema de iconos propio para calibre

Si ha creado un bonito juego de iconos y desea compartirlos con otros usuarios de calibre por medio del sistema incorporado de temas de iconos, es sencillo empaquetar los iconos en un tema. Para ello, vaya a Preferencias > Miscelánea > Crear un tema de iconos de calibre, seleccione la carpeta donde tiene los iconos. Rellene los metadatos del tema y pulse «Aceptar». Esto creará un archivo ZIP con los iconos del tema. Puede enviarlo al foro de calibre en Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] y entonces yo haré que el tema esté disponible por medio del sistema incorporado de temas de iconos. De manera predeterminada, el tema de iconos que acaba de crear también se instalará como el tema actual en calibre. Si está probando el tema, recuerde eliminar las imágenes de la carpeta resources/images, para que se usen los iconos del tema.

Desde calibre 6, puede especificar iconos personalizados para modo claro y oscuro. Cree dos versiones del icono y nombre los archivos con los sufijos -for-light-theme y -for-dark-theme, respectivamente. Por ejemplo, modificado-for-light-theme.png y modificado-for-dark-theme.png. calibre usará automáticamente el icono adecuado según el tema actual.

Personalizar calibre con complementos

calibre tiene un diseño muy modular. Casi todas las funciones de calibre vienen en forma de complementos. Los complementos se utilizan para la conversión, para la descarga de noticias (aunque éstos se llaman fórmulas), para diversos componentes de la interfaz de usuario, para conectarse a diferentes dispositivos, para procesar archivos cuando se añaden a calibre, etcétera. Puede obtener una lista completa de todos los complementos integrados en calibre en Preferencias > Avanzado > Complementos.

Puede escribir sus propios complementos para personalizar y extender el comportamiento de calibre. La arquitectura de complementos en calibre es muy simple, vea el cursillo Escribir sus propios complementos para extender la funcionalidad de calibre.

Una vez que haya escrito un complemento, puede subirlo al foro de complementos en Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237] y estará disponible a través del actualizador de complementos incorporado de calibre.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Personalizar calibre »

Documentación de la API para complementos

Documentación de la API para complementos

Define varias clases abstractas de base que pueden usarse para crear potentes complementos como subclase. Las clases útiles son:

Complemento

FileTypePlugin

Complementos de metadatos

Complementos de catálogo

Complementos de descarga de metadatos

Complementos de conversión

Controladores de dispositivo

Acciones de interfaz de usuario

Complementos de preferencias

Complemento

class calibre.customize.Plugin(plugin_path)[fuente]

Un complemento de calibre. Los miembros útiles incluyen:

self.installation_type: Almacena cómo se instaló el complemento.

self.plugin_path: Almacena la ubicación del archivo ZIP que contiene

este complemento o None si es un complemento predefinido

self.site_customization: Guarda un texto de personalización introducido

por el usuario.

Métodos que deben reemplazarse en subclases:

initialize()

customization_help()

Métodos útiles:

temporary_file()

__enter__()

load_resources()

supported_platforms = []

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

El nombre de este complemento. Debe establecer algo distinto de «Trivial Plugin» para que funcione.

version = (1, 0, 0)

La versión de este complemento como una 3-tupla (mayor, menor, revisión)

description = 'No hace absolutamente nada'

Una breve descripción de lo que hace este complemento

author = 'Desconocido'

El autor de este complemento

priority = 1

Cuando existe más de un complemento para un tipo de archivo, los complementos se ejecutarán en orden de prioridad decreciente. Los complementos de mayor prioridad se ejecutarán primero. La prioridad más alta posible es sys.maxsize. La prioridad predeterminada es 1.

minimum_calibre_version = (0, 4, 118)

La versión más antigua requerida por este complemento

installation_type = None

La manera en que se instaló este complemento

can_be_disabled = True

Si es False, el usuario no podrá deshabilitar este complemento. Usar con precaución.

type = 'Base'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

initialize()[fuente]

Se ejecuta una vez cuando se inicializan los complementos de calibre. Los complementos se vuelven a inicializar cada vez que se añade uno nuevo. Tenga en cuenta que si el complemento se ejecuta como un proceso de trabajo, por ejemplo para añadir libros, el complemento debe inicializarse por cada nuevo proceso.

Realice cualquier inicialización específica del complemento aquí, tal como extraer recursos del archivo ZIP del complemento. La ruta al archivo ZIP está disponible como self.plugin_path.

Tenga en cuenta que self.site_customization no está disponible en este punto.

config_widget()[fuente]

Implemente este método y save_settings() en el complemento para usar un cuadro de diálogo de configuración personalizado, en lugar de emplear la configuración predeterminada basada en textos.

Este método, si se implementa, debe devolver un QWidget. El widget puede tener un método opcional validate() que no toma ningún argumento y se ejecuta en cuanto el usuario pulsa el botón «Aceptar». Los cambios se aplican si y sólo si el método devuelve True.

Si por alguna razón no se puede realizar la configuración en este momento, deberá devolver una tupla de dos textos (mensaje, detalles), que se mostrarán como un cuadro de diálogo de advertencia al usuario y el proceso se cancelará.

save_settings(config_widget)[fuente]

Guardar las configuraciones especificadas por el usuario con config_widget.

Parámetros:

config_widget – El widget devuelto por config_widget().

do_user_config(parent=None)[fuente]

Este método muestra un diálogo de configuración para este complemento. Devuelve True si el usuario pulsa en «Aceptar» o False en caso contrario. Los cambios se aplican automáticamente.

load_resources(names)[fuente]

Si este complemento viene en un archivo en formato ZIP (complemento añadido por el usuario), este método le permitirá cargar recursos desde el archivo ZIP.

Por ejemplo, para cargar una imagen:

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

Parámetros:

names – Lista de rutas a los recursos en el archivo ZIP utilizando / como separador

Devuelve:

Un diccionario de la forma {nombre: contenido_del_archivo}. Cualquier nombre que no se encuentre en el archivo ZIP, no estará en el diccionario.

customization_help(gui=False)[fuente]

Devuelve un texto que ofrece ayuda sobre cómo personalizar este complemento. De manera predeterminada, genera el error NotImplementedError, que indica que el complemento no necesita personalización.

Si reimplementa este método en la subclase, se le pedirá al usuario que introduzca un texto como personalización para este complemento. El texto de personalización estará disponible como self.site_customization.

site_customization puede ser cualquier cosa, por ejemplo, la ruta de acceso a un archivo binario necesario en el equipo del usuario.

Parámetros:

gui – Si es True devuelve la ayuda HTML, de lo contrario devuelve ayuda de texto sin formato.

temporary_file(suffix)[fuente]

Devuelve un objeto de tipo archivo que es un archivo temporal en el sistema de archivos. Este archivo permanecerá disponible incluso después de cerrarse y sólo se eliminará al terminar el intérprete. Utilice el miembro name del objeto devuelto para acceder a la ruta completa del archivo temporal creado.

Parámetros:

suffix – El sufijo del archivo temporal.

cli_main(args)[fuente]

Este método es el principal punto de acceso para la interfaz de línea de órdenes de los complementos. Se ejecuta cuando el usuario teclea: calibre-debug -r «Nombre del complemento». Todos los argumentos que se pasen están presentes en la variable args.

FileTypePlugin

class calibre.customize.FileTypePlugin(plugin_path)[fuente]

Bases: Plugin

Un complemento asociado con un determinado conjunto de tipos de archivo.

file_types = {}

Conjunto de tipos de archivo para que este complemento debe ejecutarse. Use '*' para todos los tipos de archivo. Por ejemplo: {'lit', 'mobi', 'prc'}

on_import = False

Si es True, este complemento se ejecuta cuando se añaden libros a la base de datos

on_postimport = False

Si es True, este complemento se ejecuta después de añadir libros a la base de datos. En tal caso, se ejecutarán los métodos de postimport() y postadd() del complemento.

on_postconvert = False

Si es True, este complemento se ejecuta después de convertir un libro. En tal caso, se ejecutará el método postconvert() del complemento.

on_postdelete = False

Si es True, este complemento se ejecuta después de eliminar un archivo de libro de la base de datos. En tal caso, se ejecutará el método postdelete() del complemento.

on_preprocess = False

Si es True, este complemento se ejecuta justo antes de una conversión

on_postprocess = False

Si es True, este complemento se ejecuta después de la conversión sobre el archivo final producido por el complemento de salida de conversión.

type = 'Tipo de archivo'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

run(path_to_ebook)[fuente]

Ejecutar el complemento. Debe implementarse en subclases. Debe realizar cualquier modificación necesaria sobre el libro y devolver la ruta absoluta al libro modificado. Si no se necesita ninguna modificación, debe devolver la ruta de acceso al libro original. Si ocurre un error, debe generar una excepción. La implementación predeterminada simplemente devuelve la ruta de acceso al libro original. Tenga en cuenta que la ruta al archivo original (antes de que se ejecute ningún complemento de tipo de archivo) está disponible como self.original_path_to_file.

El archivo de libro electrónico modificado debe crearse con el método temporary_file().

Parámetros:

path_to_ebook – Ruta absoluta al libro electrónico.

Devuelve:

Ruta absoluta al libro electrónico modificado.

postimport(book_id, book_format, db)[fuente]

Se ejecuta después de la importación, es decir, después de añadir el archivo del libro a la base de datos. Tenga en cuenta que se trata de un método diferente de postadd(), que se ejecuta cuando se crea el registro del libro por primera vez. Este método se ejecuta siempre que se añade un archivo nuevo al registro del libro. Resulta útil para modificar el registro del libro según el contenido del archivo recién añadido.

Parámetros:

book_id – Identificador de la base de datos del libro añadido.

book_format – El tipo de archivo del libro que se ha añadido.

db – Base de datos de biblioteca.

postconvert(book_id, book_format, db)[fuente]

Se ejecuta después de la conversión, es decir, después de añadir el archivo del libro de salida a la base de datos. Tenga en cuenta que sólo se ejecuta después de una conversión, no cuando se añade un libro. Resulta útil para modificar el registro del libro según el contenido del archivo recién añadido.

Parámetros:

book_id – Identificador de la base de datos del libro añadido.

book_format – El tipo de archivo del libro que se ha añadido.

db – Base de datos de biblioteca.

postdelete(book_id, book_format, db)[fuente]

Se ejecuta después de la eliminación, es decir, después de eliminar el archivo del libro de la base de datos. Tenga en cuenta que no se ejecuta cuando se elimina un registro de libro, sólo cuando se elimina uno o más formatos del libro. Resulta útil para modificar el registro del libro según el formato del archivo eliminado.

Parámetros:

book_id – Identificador de la base de datos del libro añadido.

book_format – El tipo de archivo del libro que se ha añadido.

db – Base de datos de biblioteca.

postadd(book_id, fmt_map, db)[fuente]

Se ejecuta después la creación, es decir, después de añadir un nuevo libro a la base de datos. Tenga en cuenta que se trata de un método diferente de postimport(), que se ejecuta cuando se añade un archivo de libro a un registro. Este método se ejecuta sólo cuando se crea un nuevo registro de libro, que puede contener más de un archivo de libro. Resulta útil si desea modificar el registro del libro en la base de datos cuando se añade el libro por primera vez en calibre.

Parámetros:

book_id – Identificador de la base de datos del libro añadido.

fmt_map – Correspondencia de formato de archivo con ruta desde la que se añadió el archivo. Tenga cuenta que esto puede apuntar a un archivo existente en realidad o no, pues a veces se añaden archivos como flujos, en cuyo caso puede ser un valor inútil o una ruta no existente.

db – Base de datos de biblioteca

Complementos de metadatos

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[fuente]

Bases: Plugin

Un complemento que implementa la lectura de metadatos de un conjunto de tipos de archivo.

file_types = {}

Conjunto de tipos de archivo para los que este complemento debe ejecutarse. Por ejemplo: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

version = (7, 9, 0)

La versión de este complemento como una 3-tupla (mayor, menor, revisión)

author = 'Kovid Goyal'

El autor de este complemento

type = 'Lector de metadatos'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

get_metadata(stream, type)[fuente]

Devuelve metadatos del archivo representado por stream (un objeto de tipo archivo que admite lectura). Genera una excepción si hay un error con los datos de entrada.

Parámetros:

type – El tipo del archivo. Es con seguridad una de las entradas en file_types.

Devuelve:

Un objeto calibre.ebooks.metadata.book.Metadata.

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[fuente]

Bases: Plugin

Un complemento que implementa la lectura de metadatos de un conjunto de tipos de archivo.

file_types = {}

Conjunto de tipos de archivo para los que este complemento debe ejecutarse. Por ejemplo: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

version = (7, 9, 0)

La versión de este complemento como una 3-tupla (mayor, menor, revisión)

author = 'Kovid Goyal'

El autor de este complemento

type = 'Escritor de metadatos'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

set_metadata(stream, mi, type)[fuente]

Establece los metadatos del archivo representado por stream (un objeto de tipo archivo que admite lectura). Genera una excepción si hay un error con los datos de entrada.

Parámetros:

type – El tipo del archivo. Es con seguridad una de las entradas en file_types.

mi – Un objeto calibre.ebooks.metadata.book.Metadata.

Complementos de catálogo

class calibre.customize.CatalogPlugin(plugin_path)[fuente]

Bases: Plugin

Un complemento que implementa un generador de catálogos.

file_types = {}

Tipo de archivo de salida para el que debe ejecutarse este complemento. Por ejemplo: «epub» o «xml»

type = 'Generador de catálogo'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

cli_options = []

Opciones del intérprete de línea de órdenes específicas de este complemento, declaradas como un namedtuple Option:

from collections import namedtuple Option = namedtuple(“Option”, “option, default, dest, help”) cli_options = [Option(”–catalog-title”, default = “My Catalog”, dest = “catalog_title”, help = (_(“Title of generated catalog. nDefault:”) + « “» + “%default” + «”»))] cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()

initialize()[fuente]

Si el complemento no es uno de los incorporados, copiar los archivos .ui y .py del archivo ZIP a $TMPDIR. Se generará dinámicamente una pestaña que se añadirá a las opciones de catálogo en calibre.gui2.dialogs.catalog.py:Catalog

run(path_to_output, opts, db, ids, notification=None)[fuente]

Ejecutar el complemento. Debe implementarse en subclases. Debe generar el catálogo en el formato especificado en file_types y devolver la ruta de acceso absoluta al archivo de catálogo generado. Si ocurre un error, debe generar una excepción.

El archivo del catálogo generado debe crearse con el método temporary_file().

Parámetros:

path_to_output – Ruta absoluta al archivo de catálogo generado.

opts – Un diccionario de argumentos de palabras claves

db – Un objeto LibraryDatabase2

Complementos de descarga de metadatos

class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[fuente]

Bases: Plugin

type = 'Origen de metadatos'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

author = 'Kovid Goyal'

El autor de este complemento

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

capabilities = frozenset({})

Conjunto de capacidades ofrecidas por este complemento. Algunas capacidades útiles son: “identify”, “cover”

touched_fields = frozenset({})

Lista de campos de metadatos que este complemento puede descargarse durante la fase de identificación.

has_html_comments = False

Establézcala a True si el complemento devuelve comentarios en formato HTML

supports_gzip_transfer_encoding = False

Si se establece en True el objeto de navegador indicará que admite transferencias codificadas con gzip. Esto puede acelerar las descargas, pero asegúrese primero de que el origen admite transferencias codificadas con gzip correctamente

ignore_ssl_errors = False

Si se establece en true se ignoran los errores de certificado HTTPS al conectar con este origen.

cached_cover_url_is_reliable = True

Los URL de portadas en caché no siempre son fiables (la descarga puede fallar o la imagen puede ser incorrecta). Si esto ocurre a menudo con este origen, establézcalo en False

options = ()

Una lista de objetos Option. Se usarán para construir automáticamente el widget de configuración para este complemento

config_help_message = None

Un texto que se muestra en la parte superior del widget de configuración de este complemento.

can_get_multiple_covers = False

Si es True este recurso puede devolver múltiples portadas para una consulta dada

auto_trim_covers = False

Si se establece en True, las portadas descargadas por este complemento se recortan automáticamente.

prefer_results_with_isbn = True

Si se establece en True, y esta fuente devuelve múltiples resultados para una consulta, algunos de los cuales tienen ISBN y otros no, los resultados sin ISBN se ignorarán

is_configured()[fuente]

Devuelve False si el complemento necesita configurarse antes de usarlo. Por ejemplo, puede requerir un nombre de usuario, contraseña o clave API.

customization_help()[fuente]

Devuelve un texto que ofrece ayuda sobre cómo personalizar este complemento. De manera predeterminada, genera el error NotImplementedError, que indica que el complemento no necesita personalización.

Si reimplementa este método en la subclase, se le pedirá al usuario que introduzca un texto como personalización para este complemento. El texto de personalización estará disponible como self.site_customization.

site_customization puede ser cualquier cosa, por ejemplo, la ruta de acceso a un archivo binario necesario en el equipo del usuario.

Parámetros:

gui – Si es True devuelve la ayuda HTML, de lo contrario devuelve ayuda de texto sin formato.

config_widget()[fuente]

Implemente este método y save_settings() en el complemento para usar un cuadro de diálogo de configuración personalizado, en lugar de emplear la configuración predeterminada basada en textos.

Este método, si se implementa, debe devolver un QWidget. El widget puede tener un método opcional validate() que no toma ningún argumento y se ejecuta en cuanto el usuario pulsa el botón «Aceptar». Los cambios se aplican si y sólo si el método devuelve True.

Si por alguna razón no se puede realizar la configuración en este momento, deberá devolver una tupla de dos textos (mensaje, detalles), que se mostrarán como un cuadro de diálogo de advertencia al usuario y el proceso se cancelará.

save_settings(config_widget)[fuente]

Guardar las configuraciones especificadas por el usuario con config_widget.

Parámetros:

config_widget – El widget devuelto por config_widget().

get_author_tokens(authors, only_first_author=True)[fuente]

Toma una lista de autores y devuelve una lista de elementos útiles para una consulta de búsqueda AND. Esta función intenta devolver elementos con el orden «nombres apellidos», suponiendo que si hay una coma en nombre de autor, el nombre está en la forma «apellidos, nombre».

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[fuente]

Toma un título y devuelve una lista de elementos útiles para una consulta de búsqueda AND. Excluye conectores (opcionalmente) y puntuación.

split_jobs(jobs, num)[fuente]

Divide una lista de tareas en num grupos como máximo, tan igualados como sea posible

test_fields(mi)[fuente]

Devuelve el primer campo de self.touched_fields que es nulo en el objeto mi

clean_downloaded_metadata(mi)[fuente]

Ejecutar este métedo en el método de identificación del complemento para normalizar los metadatos antes de poner el objeto mi en result_queue. Lógicamente puede usar un algoritmo personalizado adecuado a la fuente de metadatos.

get_book_url(identifiers)[fuente]

Devuelve una 3-tupla o None. La 3-tupla es de la forma: (tipo_identificador, valor_identificador, URL). URL es el URL del libro identificado por los identificadores en este origen. tipo_identificador y valor_identificador especifican el identificador correspondiente al URL. Este URL debe ser accesible a un humano por medio de un navegador. El propósito es proporcionar un enlace que el usuario pueda pulsar para visitar la página del libro en este origen. Si no se encuentra ningún URL, devuelve None. Este método debe ser rápido y coherente, por lo que sólo debe implementarlo si es posible construir el URL mediante un esquema conocido dado identifiers.

get_book_url_name(idtype, idval, url)[fuente]

Devuelve un nombre legible por humanos a partir valor devuelto por get_book_url().

get_book_urls(identifiers)[fuente]

Sustituya este método si desea devolver varios URL para este libro. Devuelve una lista de 3-tuplas. De manera predeterminada, este método simplemente ejecuta get_book_url().

get_cached_cover_url(identifiers)[fuente]

Devuelve el URL de portada en caché para el libro identificado por el diccionario identifiers o None si no existe el URL.

Tenga en cuenta que este método sólo debe devolver URL validados, es decir no URL que puedan resultar en una imagen de portada genérica o un error.

id_from_url(url)[fuente]

Analiza un URL y devuelve una tupla de la forma: (tipo_de_identificador, valor_de_identificador). Si el URL no coincide con el patrón del origen de metadatos, devuelve None.

identify_results_keygen(title=None, authors=None, identifiers={})[fuente]

Devuelve una función empleada para generar una clave que pueda ordenar objetos de tipo Metadata por su relevancia dada una consulta de búsqueda (title, authors, identifiers).

Estas claves se usan para ordenar los resultados de identify().

Para detalles sobre el algoritmo predeterminado ver InternalMetadataCompareKeyGen. Implemente de nuevo esta función en el complemento si el algoritmo predeterminado no es el adecuado.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[fuente]

Identificar un libro por su título, autor, ISBN, etc.

Si se especifica identifiers y no se encuentran coincidencias y este origen de metadatos no almacena todos los identificadores relacionados (por ejemplo, todos los ISBN de un libro), este método debe volver a intentarlo con sólo el título y el autor (si se especificaron).

Si este origen de metadatos también proporciona portadas, el URL de la portada debe almacenarse en caché para que ejecuciones posteriores a la API para obtener portadas con el mismo ISBN o identificador especial no tenga que volver a obtener el URL de portada. Usar la API de caché para esto.

Cada objeto Metadata puesto en result_queue por este método debe tener un atributo attr:source_relevance que es un entero que indica el orden en que el origen de metadatos devuelve los resultados para esta búsqueda. Este entero se usará por compare_identify_results(). Si el orden no es importante, póngalo a cero para todos los resultados.

Asegúrese de que cualquier información de correspondencia de portada o ISBN está en caché antes de poner el objeto Metadata en result_queue.

Parámetros:

log – Un objeto de registro, úselo para obtener errores e información de depuración

result_queue – Un objeto Queue resultante, los resultados deben ser puestos en él. Cada resultado es un objeto Metadata.

abort – Si abort.is_set() devuelve True, interrumpir el proceso y volver tan pronto como sea posible

title – El título del libro, puede ser None

authors – Una lista de autores del libro, puede ser None

identifiers – Un diccionario de otros identificadores, principalmente {“isbn”:”1234…”}

timeout – Tiempo de espera en segundos, ninguna petición de red debería esperar más de este tiempo.

Devuelve:

None si no hubo ningún error, en caso contrario una representación unicode del error que pueda mostrarse al usuario

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[fuente]

Descarga una portada y pone el resultado en result_queue. Todos los parámetros tienen el mismo significado que para identify(). Pone (self, cover_data) en result_queue.

Este metodo de usar los URL de portada en caché siempre que sea posible. Cuando no se encuentran los datos en caché, la mayoría de los complementos ejecutan identify() y usan sus resultados.

Si el parámetro get_best_cover es True y este complemento puede obtener múltiples portadas, esto debería obtener sólo la «mejor».

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[fuente]

Generar una clave de orden para comparar la relevancia de los objetos Metadata, dada una consulta de búsqueda. Esto se usa sólo para comparar resultados del mismo origen de metadatos, no entre distintos orígenes.

La clave de orden garantiza que el orden ascendente corresponde a un orden descendiente de relevancia.

El algoritmo es:

Dar prioridad a los resultados que tienen al menos un identificador igual al de la consulta

Preferir resultados con un URL de portada en caché

Preferir resultados con todos los campos disponibles rellenos

Dar prioridad a los resultados en el mismo idioma que la interfaz de usuario actual

Preferir resultados con una coincidencia de título perfecto con la consulta

Preferir resultados con comentarios más largos (diferencia mayor del 10%)

Usar la relevancia del resultado según la indice la búsqueda del origen de metadatos

motor

Complementos de conversión

class calibre.customize.conversion.InputFormatPlugin(*args)[fuente]

Bases: Plugin

Los complementos InputFormatPlugin son los responsables de convertir un documento a HTML+OPF+CSS+etc. Los resultados de la conversión deben estar codificados en UTF-8. La acción principal ocurre en convert().

type = 'Entrada para la conversión'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

can_be_disabled = False

Si es False, el usuario no podrá deshabilitar este complemento. Usar con precaución.

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

file_types = {}

Conjunto de tipos de archivo para los que este complemento debe ejecutarse. Por ejemplo: set(['asw', 'mobi', 'prc'])

is_image_collection = False

Si es True, este complemento de entrada genera una colección de imágenes, una por archivo HTML. Esto puede ser establecido dinámicamente en el método convert() si los archivos de entrada pueden ser o no colecciones de imágenes. Si lo establece en True, debe implementar el método get_images() que devuelve una lista de imágenes.

core_usage = 1

Número de núcleos de CPU utilizados por este complemento. Un valor de -1 significa que utiliza todos los núcleos disponibles

for_viewer = False

Si es True, el complemento de entrada realizará un procesado especial para que la salida sea adecuada para visualización

output_encoding = 'utf-8'

La codificación de los archivos que crea el complemento de entrada. Un valor de None significa que la codificación no está definida y debe detectarse individualmente

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Opciones compartidas por todos los complementos de formato de entrada. No reemplazar en subclases. Usar options en su lugar. Toda opción debe ser un ejemplar de OptionRecommendation.

options = {}

Opciones para personalizar el comportamiento de este complemento. Toda opción debe ser un ejemplar de OptionRecommendation.

recommendations = {}

Un conjunto de 3-tuplas del tipo (nombre_opción, valor_recomendado, nivel_de_recomendación)

get_images()[fuente]

Devuelve una lista de rutas absolutas a las imágenes, si este complemento representa una colección de imágenes. La lista de imágenes está en el mismo orden que el lomo y el índice.

convert(stream, options, file_ext, log, accelerators)[fuente]

Este método debe ser implementado en subclases. Debe volver la ruta al archivo OPF creado o un ejemplar OEBBook. Toda la salida debe estar contenida en la carpeta actual. Si este complemento crea archivos fuera de la carpeta actual, deben ser eliminados o marcados para su eliminación antes de que termine este método.

Parámetros:

stream – Un objeto de tipo archivo que contiene el archivo de entrada.

options – Opciones para personalizar el proceso de conversión. Debe tener atributos correspondientes a todas las opciones declaradas por este complemento. Además, debe haber un atributo verbose que toma valores enteros a partir de cero. Valores mayores significan más detalles. Otro atributo útil es input_profile, que es un ejemplar de calibre.customize.profiles.InputProfile.

file_ext – La extensión (sin el «.») del archivo de entrada. Debe ser uno de los file_types admitidos por este complemento.

log – Un objeto calibre.utils.logging.Log. Toda salida debería usar este objeto.

accelarators – Un diccionario con diversa información que el complemento de entrada puede obtener fácilmente y que acelerará las etapas posteriores de la conversión.

postprocess_book(oeb, opts, log)[fuente]

Se ejecuta para permitir que el complemento de entrada lleve a cabo el posprocesado del libro después del procesado principal.

specialize(oeb, opts, log, output_fmt)[fuente]

Se ejecuta para permitir que el complemento de entrada especialice el libro analizado para un formato de salida particular. Se ejecuta después de postprocess_book() y antes de que se realice cualquier transformación sobre el libro analizado.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[fuente]

Ejecutado para crear el widget que se usa para configurar este complemento en la interfaz de calibre. El widget debe ser un objeto de la clase PluginWidget. Puede ver ejemplos en los complementos de entrada incorporados.

class calibre.customize.conversion.OutputFormatPlugin(*args)[fuente]

Bases: Plugin

Los complementos OutputFormatPlugin son los responsables de convertir un documento OEB (OPF+HTML) en un libro de salida.

El documento OEB puede suponerse codificado en UTF-8. La acción principal ocurre en convert().

type = 'Salida de la conversión'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

can_be_disabled = False

Si es False, el usuario no podrá deshabilitar este complemento. Usar con precaución.

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

file_type = None

El tipo de archivo (extensión sin punto inicial) que produce este complemento

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Opciones compartidas por todos los complementos de formato de entrada. No reemplazar en subclases. Usar options en su lugar. Toda opción debe ser un ejemplar de OptionRecommendation.

options = {}

Opciones para personalizar el comportamiento de este complemento. Toda opción debe ser un ejemplar de OptionRecommendation.

recommendations = {}

Un conjunto de 3-tuplas del tipo (nombre_opción, valor_recomendado, nivel_de_recomendación)

property description

str(object=””) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Crea un nuevo objeto de texto a partir de object. Si se especifica encoding o errors, el objeto debe exponer un buffer de datos que se descodificará con la codificación y controlador de errores especificados. En caso contrario, devuelve el resultado de object.__str__()` (si está definido) o ``repr(object). El valor predeterminado de encoding es sys.getdefaultencoding(). El valor predeterminado de errors es 'strict'.

convert(oeb_book, output, input_plugin, opts, log)[fuente]

Mostrar el contenido de oeb_book (que es un espécimen de calibre.ebooks.oeb.OEBBook) en el archivo especificado por salida.

Parámetros:

output – Un objeto de tipo archivo o un texto. Si es un texto es la ruta a una carpeta que puede existir o no. El complemento de salida debe escribir su resultado en esta carpeta. Si es un objeto de tipo archivo, el complemento de salida debe escribir su resultado en el archivo.

input_plugin – El archivo de entrada que se usó al inicio del proceso de conversión.

opts – Opciones de conversión. Es seguro que tiene atributos correspondientes a los valores de OptionRecommendations de este complemento.

log – El registrador. Escribir mensajes de depuración, información, etc. usando este objeto.

specialize_options(log, opts, input_fmt)[fuente]

Puede usarse para modificar los valores de las opciones de conversión, como en el proceso de conversión.

specialize_css_for_output(log, opts, item, stylizer)[fuente]

Puede usarse para hacer cambios al css durante el proceso de aplanamiento del CSS.

Parámetros:

item – El elemento (archivo HTML) que se está procesando.

stylizer – Un objeto de tipo Stylizer que contiene los estilos aplanados para item. Puede obtener el estilo para cualquier elemento con ``stylizer.style(elemento)`.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[fuente]

Ejecutado para crear el widget que se usa para configurar este complemento en la interfaz de calibre. El widget debe ser un objeto de la clase PluginWidget. Puede ver ejemplos en los complementos de salida incorporados.

Controladores de dispositivo

La clase base para todos los controladores de dispositivo es DevicePlugin. Sin embargo, si el dispositivo se muestra como una unidad USBMS ante el sistema operativo, debe usar la clase USBMS en su lugar, pues implementa toda la lógica necesaria para este tipo de dispositivos.

class calibre.devices.interface.DevicePlugin(plugin_path)[fuente]

Bases: Plugin

Define la interfaz que deben implementar los motores que comunican con un lector de libros electrónicos.

type = 'Interfaz del dispositivo'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

Lista ordenada de formatos soportados

VENDOR_ID = 0

VENDOR_ID puede ser un entero, una lista de enteros o un diccionario. Si es un diccionario, debe ser un diccionario de diccionarios de la forma:

{
 integer_vendor_id : { product_id : [list of BCDs], ... },
 ...
}

PRODUCT_ID = 0

Un entero o una lista de enteros

BCD = None

BCD puede ser bien None para no distinguir entre dispositivos según BCD, o bien una lista de los números BCD de todos los dispositivos soportados por este controlador.

THUMBNAIL_HEIGHT = 68

Altura de las miniaturas en el dispositivo

THUMBNAIL_COMPRESSION_QUALITY = 75

Calidad de compresión para las miniaturas. Cuanto más cercano a 100, mejor será la calidad de las miniaturas y menores los defectos de compresión. Por supuesto, también ocuparán más espacio las miniaturas.

WANTS_UPDATED_THUMBNAILS = False

Establézcalo en True si el dispositivo admite actualizar miniaturas de portada durante meth:sync_booklists. Si es True, se pedirá a device.py que vuelva a cargar las miniaturas al comparar libros

CAN_SET_METADATA = ['title', 'authors', 'collections']

Especifica si los metadatos de los libros pueden establecerse a través de la interfaz gráfica.

CAN_DO_DEVICE_DB_PLUGBOARD = False

Especifica si el dispositivo puede gestionar paneles de conexiones de metadatos device_db

path_sep = '/'

Separador de ruta para rutas de acceso a los libros en el dispositivo

icon = 'reader.png'

Icono para este dispositivo

UserAnnotation

alias de Annotation

OPEN_FEEDBACK_MESSAGE = None

La interfaz gráfica muestra esto como un mensaje en la barra de estado si no es None. Útil si la apertura puede llevar mucho tiempo

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

Conjunto de extensiones que son «libros virtuales» en el dispositivo y por lo tanto no pueden visualizarse, guardarse o añadirse a la biblioteca. Por ejemplo: frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

Mensaje que se mostrará al usuario para las extensiones de libro virtuales.

NUKE_COMMENTS = None

Indica si se eliminan los comentarios de la copia del libro que se envía al dispositivo. Si no es None, debería ser un texto corto por el que se sustituirán los comentarios.

MANAGES_DEVICE_PRESENCE = False

Si es True, indica que este controlador gestiona completamente la detección de dispositivos, la desconexión, etc. Si la establece a True, debe implementar los métodos detect_managed_devices y debug_managed_device_detection. Un controlador con esta variable como True tiene la responsabilidad de detectar dispositivos, gestionar una lista negra de dispositivos, una lista de dispositivos desconectados, etc. calibre ejecutará periódicamente el método meth:detect_managed_devices() y si devuleve un dispositivo detectado, calibre ejecutará open(). open() se ejecutará cada vez que se devuelva un dispositivo, incluso si previamente open() falló, por lo tanto el controlador debe mantener su propia lista negra de dispositivos fallidos. Análogamente, al desconectar, calibre ejecutará eject() y, suponiendo que que la siguiente ejecución de detect_managed_devices() devuelva None, ejecutará post_yank_cleanup().

SLOW_DRIVEINFO = False

Si se establece en True, calibre ejecutará el método get_driveinfo() una vezs cargadas las listas de libros para obtener la información de la unidad.

ASK_TO_ALLOW_CONNECT = False

Si se establece en True, calibre preguntará al usuario si quiere administrar el dispositivo con calibre la primera vez que se detecta. Si lo establece en True debe implementar get_device_uid(), ignore_connected_device(), get_user_blacklisted_devices() y set_user_blacklisted_devices()

user_feedback_after_callback = None

Establezca esto a un diccionario de la forma {“title”:título, “msg”:mensaje, “det_msg”:mensaje_detallado} para que calibre muestre una ventana con un mensaje para el usuario tras ejecutar varias acciones (actualmente sólo upload_books()). Procure no mostrar demasiados mensajes al usuario. Esta variable se comprueba después de cada acción, así que establézcala sólo cuando sea realmente necesaria.

classmethod get_open_popup_message()[fuente]

La interfaz gráfica muestra esto como una ventana emergente no modal. Debe ser un ejemplar de OpenPopupMessage

is_usb_connected(devices_on_system, debug=False, only_presence=False)[fuente]

Devuelve True, device_info si algún dispositivo gestionado por este complemento está actualmente conectado.

Parámetros:

devices_on_system – Lista de dispositivos conectados actualmente

detect_managed_devices(devices_on_system, force_refresh=False)[fuente]

Sólo se llama si MANAGES_DEVICE_PRESENCE es True.

Buscar dispositivos que pueda gestionar este controlador. Debe devolver un objeto de dispositivo si se encuentra algún dispositivo. Este objeto se pasará al método open() como connected_device. Si no se encuentra ningún dispositivo, devuelve None. El objeto devuelto puede ser cualquier cosa, calibre no lo usa, sólo lo pasa a open().

Este método se ejecuta periódicamente por la interfaz gráfica, así que asegúrese de que no requiere demasiados recursos. Use un caché para evitar buscar en el sistema una y otra vez.

Parámetros:

devices_on_system – Conjunto de dispositivos USB encontrados en el sistema.

force_refresh – Si es True y el controlador usa un caché para evitar búsquedas repetitivas, el caché debe vaciarse.

debug_managed_device_detection(devices_on_system, output)[fuente]

Sólo se llama si MANAGES_DEVICE_PRESENCE es True.

Debe escribir en output información sobre los dispositivos detectados en el sistema, que es un objeto de tipo archivo.

Debe devolver True si se detecta un dispositivo y se abre con éxito, en caso contrario debe devolver False.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[fuente]

Parámetros:

key – La clave para desbloquear el dispositivo

log_packets – Si es True, el flujo de paquetes de o al dispositivo se registra

report_progress – Función que se ejecuta con un argumento de progreso en porcentaje (número entre 0 y 100) para diversas tareas. Si el argumento es -1 significa que la tarea no tiene información de progreso.

detected_device – Información de dispositivo desde el examinador de dispositivos

can_handle_windows(usbdevice, debug=False)[fuente]

Método opcional para comprobar si este controlador puede manejar un dispositivo mediante más pruebas. Si no puede, debe devolver False. Este método sólo se ejecuta una vez que los identificadores del fabricante y del producto, así como el BCD se han reconocido, por lo que puede realizar pruebas que requieran un tiempo relativamente prolongado. La implementación predeterminada devuelve True. Este método sólo se ejecuta en Windows. Véase también can_handle().

Tenga en cuenta que para dispositivos basados en USBMS este método de manera predeterminada delega en can_handle(). Así que sólo necesita sustituir can_handle() en la subclase de USBMS.

Parámetros:

usbdevice – Un dispositivo usb devuelto por calibre.devices.winusb.scan_usb_devices()

can_handle(device_info, debug=False)[fuente]

Versión unix de can_handle_windows().

Parámetros:

device_info – Es una tupla de (id_fab, id_prod, bcd, fabricante, producto, número de serie)

open(connected_device, library_uuid)[fuente]

Realiza cualquier inicio específico del dispositivo. Se ejecuta una vez que se ha detectado el dispositivo, pero antes de cualquier otra función que comunique con él. Por ejemplo, para dispositivos que se muestran como dispositivos de almacenamiento masivo USB, este método será el responsable de montar el dispositivo o, si se ha montado automáticamente, averiguar dónde. El método calibre.devices.usbms.device.Device.open() tiene una implementación de esta función que puede ser un buen ejemplo para dispositivos de almacenamiento masivo USB.

Este método puede generar una excepción de tipo OpenFeedback para mostrar un mensaje al usuario final.

Parámetros:

connected_device – El dispositivo que se intenta abrir. Es una tupla de (identificador de fabricante, identificador de producto, bcd, nombre de fabricante, nombre de producto, número de serie del dispositivo). Sin embargo, algunos dispositivos no tienen número de serie y en Windows sólo aparecen los tres primeros campos, el resto son None.

library_uuid – El UUID de la biblioteca de calibre actual. Puede ser None si no existe una biblioteca (por ejemplo cuando se usa desde la línea de órdenes).

eject()[fuente]

Desmontar o expulsar el dispositivo del sistema operativo. Esto no comprueba si hay tareas de interfaz gráfica pendientes que tengan que comunicar con el dispositivo.

NOTA: Este método no puede ejecutarse en el mismo subproceso que el resto de los métodos de dispositivo.

post_yank_cleanup()[fuente]

Se ejecuta si el usuario desconecta el dispositivo sin expulsarlo primero.

set_progress_reporter(report_progress)[fuente]

Establece una función para mostrar información de progreso.

Parámetros:

report_progress – Función que se ejecuta con un argumento de progreso en porcentaje (número entre 0 y 100) para diversas tareas. Si el argumento es -1 significa que la tarea no tiene información de progreso.

get_device_information(end_session=True)[fuente]

Pregunta al dispositivo por su información interna. Ver L{DeviceInfoQuery}.

Devuelve:

(nombre del dispositivo, versión del dispositivo, versión del software en el dispositivo, tipo MIME) La tupla puede tener un quinto elemento opcional, que es un diccionario de información de unidad. Puede verse un ejemplo en usbms.driver.

get_driveinfo()[fuente]

Devuelve el diccionario de información de dispositivo. Normalmente se ejecuta desde get_device_information(), pero si la carga de la información del dispositivo es lenta para este controlador, debería establecer SLOW_DRIVEINFO. En este caso, calibre ejecutará este método después de cargar la lista de libros. Tenga en cuenta que no se ejecuta en el hilo del dispositivo, por lo que el controlador debería almacenar en caché la información del dispositivo en el método books() y esta función debería devolver los datos almacenados.

card_prefix(end_session=True)[fuente]

Devuelve una lista de dos elementos con los prefijos para las rutas de acceso en las tarjetas. Si no hay tarjeta, el prefijo correspondiente será None. Por ejemplo: (“/lugar”, “/lugar2”) (None, “lugar2”) (“lugar”, None) (None, None)

total_space(end_session=True)[fuente]

Obtiene el espacio total disponible en los puntos de montaje:

Memoria principal

Tarjeta de almacenamiento A

Tarjeta de almacenamiento B

Devuelve:

Una lista de tres elementos con el espacio total en bytes de (1, 2, 3). Si un dispositivo concreto no tiene alguna de estas ubicaciones, debe devolver 0.

free_space(end_session=True)[fuente]

Obtiene el espacio libre disponible en los puntos de montaje:

Memoria principal

Tarjeta A

Tarjeta B

Devuelve:

Una lista de tres elementos con el espacio libre en bytes de (1, 2, 3). Si un dispositivo concreto no tiene alguna de estas ubicaciones, debe devolver -1.

books(oncard=None, end_session=True)[fuente]

Devuelve una lista de los libros electrónicos en el dispositivo.

Parámetros:

oncard – Si es “carda” o “cardb”, devuelve una lista de los libros en la tarjeta de almacenamiento especificada, en caso contrario devuelve una lista de los libros en la memoria principal del dispositivo. Si se especifica una tarjeta y no hay libros en ella, devuelva una lista vacía.

Devuelve:

Un objeto BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[fuente]

Copia una lista de libros al dispositivo. Si un archivo ya existe en el dispositivio, deberá ser sustituido. Este método debería generar un error FreeSpaceError si no hay suficiente espacio libre en el dispositivo. El texto del error FreeSpaceError debe contener la palabra «card» si ``on_card` no es None, si no, debe contener la palabra «memory».

Parámetros:

files – Una lista de rutas

names – Una lista de nombres de archivo que los libros deberán tener una vez copiados al dispositivo. len(names) == len(files)

metadata – Si no es None, es una lista de objetos Metadata. La idea es utilizar los metadatos para determinar dónde poner el libro en el dispositivo. len(metadata) == len(files). Aparte de la portada normal (ruta a la portada), también puede haber un atributo de miniatura (thumbnail), que debería tener prioridad. El atributo thumbnail es de la forma (anchura, altura, datos_de_portada como jpeg).

Devuelve:

Una lista de tuplas de 3 elementos. La lista se envía a add_books_to_metadata().

classmethod add_books_to_metadata(locations, metadata, booklists)[fuente]

Añade ubicaciones a las listas de libros. Esta función no debe comunicarse con el dispositivo.

Parámetros:

locations – Resultado de una llamada a L{upload_books}

metadata – Lista de objetos Metadata, igual que para upload_books().

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

delete_books(paths, end_session=True)[fuente]

Borrar libros en ubicaciones del dispositivo.

classmethod remove_books_from_metadata(paths, booklists)[fuente]

Elimina libros de la lista de metadatos. Esta función no debe comunicarse con el dispositivo.

Parámetros:

paths – rutas a los libros en el dispositivo.

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

sync_booklists(booklists, end_session=True)[fuente]

Actualizar metadatos del dispositivo.

Parámetros:

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

get_file(path, outfile, end_session=True)[fuente]

Lee el archivo en path en el dispositivo y lo escribe en outfile.

Parámetros:

outfile – objeto de archivo como sys.stdout o el resultado de ejecutar open().

classmethod config_widget()[fuente]

Debe devolver un QWidget. El QWidget contiene las configuraciones de la interfaz de dispositivo.

classmethod save_settings(settings_widget)[fuente]

Debe guardar las configuraciones en el disco. Toma el widget creado en config_widget() y guarda todas las configuraciones en el disco.

classmethod settings()[fuente]

Debe devolver un objeto opts. El objeto opts debería tener al menos un atributo format_map que es una lista ordenada de formatos para el dispositivo.

set_plugboards(plugboards, pb_func)[fuente]

Proporciona al controlador el conjunto actual de paneles de conexiones y una función para seleccionar un panel de conexión específico. Este método se ejecuta inmediatamente antes de add_books y sync_booklists.

pb_func es un metodo ejecutable con la siguiente firma:

def pb_func(device_name, format, plugboards)

Le da el nombre del dispositivo actual (ya sea el nombre de la clase o DEVICE_PLUGBOARD_NAME), el formato en que está interesado (un formato «real» o `device_db`), y los paneles de conexiones (suministrados por set_plugboards, en el mismo lugar de donde se obtuvo este método).

Devuelve:

None o un único panel de conexiones.

set_driveinfo_name(location_code, name)[fuente]

Establece el nombre de dispositivo en el archivo driveinfo como name. Este ajuste persistirá hasta que el archivo se vuelva a crear o se cambie el nombre de nuevo.

Los dispositivos que no son de disco deberían implementar este método según los códigos de ubicación devueltos por el método get_device_information().

prepare_addable_books(paths)[fuente]

Dada una lista de rutas de acceso, devuelve otra lista de rutas de acceso. Estas rutas apuntan a versiones de los libros que pueden añadirse.

Si ocurre un error al preparar un libro, en lugar de una ruta de acceso, la posición en la lista devuelta para ese libro debería de ser una tupla de tres elementos: (ruta_original, la excepción, rastro)

startup()[fuente]

Se ejecuta cuando calibre inicia el dispositivo. Realiza cualquier inicialización necesaria. Tenga en cuenta que pueden generarse varios especímenes de la clase, y por lo tanto __init__() puede ejecutarse varias veces, pero sólo un espécimen ejecutará este método. Este método se ejecuta en el hilo del dispositivo, no en el de la interfaz gráfica.

shutdown()[fuente]

Se ejecuta cuando calibre se está apagando, ya sea definitivamente o como preparación para reiniciarse. Lleva a cabo cualquier limpieza requerida. Este método se ejecuta en el hilo del dispositivo, no en el de la interfaz gráfica.

get_device_uid()[fuente]

Debe devolver un identificador único para el dispositivo conectado actualmente (se ejecuta inmediatamente después de ejecutarse open() con éxito). Debe implementar este método si establece ASK_TO_ALLOW_CONNECT = True.

ignore_connected_device(uid)[fuente]

El dispositivo identificado por uid (el resultado de ejecutar get_device_uid()) debe ignorarse en el futuro. Debe implementar este método si establece ASK_TO_ALLOW_CONNECT = True. Tenga en cuenta que esta función se ejecuta inmediatamente después de open(), así que si open() almacena algún estado en caché el controlador debería restablecer el estado.

get_user_blacklisted_devices()[fuente]

Devuelve un diccionario de identificadores y nombres de dispositivo para todos los dispositivos que el usuario ha pedido ignorar.

set_user_blacklisted_devices(devices)[fuente]

Establecer la lista de uids de dispositivo que deben ser ignorados por este controlador.

specialize_global_preferences(device_prefs)[fuente]

Implemente este método si el dispositivo quiere sustituir una preferencia particular. Debe asegurarse de que todos los lugares de ejecución que emplean una preferencia que pueda ser sustituida usen device_prefs['algo'] en lugar de prefs['algo']. El metodo debe ejecutar device_prefs.set_overrides(pref=val, pref=val, ...). Actualmente se usa para: gestión de metadatos (prefs['manage_device_metadata']).

set_library_info(library_name, library_uuid, field_metadata)[fuente]

Implemente este método si quiere información sobre la biblioteca de calibre actual. Este método se ejecuta al inicio y cuando la biblioteca de calibre cambia mientras está conectado.

is_dynamically_controllable()[fuente]

Ejecutado por el administrador de dispositivos al iniciar complementos. Si este método devuelve un texto, entonces a) soporta la interfaz de control dinámica del administrador de dispositivos y b) debe usarse ese nombre para comunicarse con el complemento.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

start_plugin()[fuente]

Este método se ejecuta para iniciar el complemento. El complemento debe empezar a aceptar conexiones de dispositivos de la manera que lo haga. Si el complemento ya acepta conexiones, no debe hacer nada.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

stop_plugin()[fuente]

Este método se ejecuta para detener el complemento. El complemento debe dejar de aceptar conexiones y debe hacer hacer limpieza tras de sí. Probablemente este método debería ejecutar shutdown(). Si el complemento ya no acepta conexiones, no debe hacer nada.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

get_option(opt_string, default=None)[fuente]

Devuelve el valor de la opción indicada por opt_string. Este método puede ejecutarse cuando el complemento no se ha iniciado. Devuelve None si la opción no existe.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

set_option(opt_string, opt_value)[fuente]

Establece el valor de la opción indicada por opt_string. Este método puede ejecutarse cuando el complemento no se ha iniciado.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

is_running()[fuente]

Devuelve True si el complemento está iniciado, de lo contrario False.

Este método puede ejecutarse en el hilo de la interfaz gráfica. Un controlador que implemente este método debe ser seguro para subprocesos.

synchronize_with_db(db, book_id, book_metadata, first_call)[fuente]

Se ejecuta durante el emparejamiento de libros, cuando cada libro del dispositivo se empareja con un libro en la base de datos de calibre. El método tiene por cometido sincronizar los datos del dispositivo con la base de datos de calibre (si es necesario).

Este método debe devolver una tupla de dos valores. El primer valor es un conjunto de identificadores de libro de calibre modificados si se modificó la base de datos de calibre, o None si la base de datos no se modificó. Si el primer valor es un conjunto vacío, los metadatos del libro en el dispositivo se actualizan con los metadatos de calibre y se vuelven a mandar al dispositivo, pero no se actualiza la interfaz gráfica para ese libro. Esto es útil cuando los metadatos de calibre son correctos, pero deben enviarse al dispositivo.

El segundo valor es una tupla de 2 valores. El primer valor de la tupla especifica si debe enviarse un formato de libro al dispositivo. El propósito es permitir comprobar que el libro en el dispositivo es el mismo que en calibre. Este valor debe ser None si no debe enviarse ningún libro, en caso contrario debe devolver el nombre de archivo base en el dispositivo (un texto como «foobar.epub»). Asegúrese de incluir la extensión en el nombre. El subsistema del dispositivo generará una tarea send_books para todos los libros cuyo valor devuelto no sea None. Nota: salvo para extraer la extensión, el nombre de archivo no tiene efecto en los casos en que el dispositivo usa una plantilla para generar el nombre de archivo, lo que ocurre para la mayoría. El segundo valor en la tupla devuelta indica si el formato está fechado en el futuro. Devuelve True si lo está, en caso contrario devuelve False. calibre mostrará un cuadro de diálogo con todos los libros fechados en el futuro.

Extremadamente importante: este método se ejecuta en el subproceso de la interfaz gráfica. Debe ser seguro con respecto al subproceso del administrador de dispositivos.

book_id: el identificador de calibre para el libro en la base de datos. book_metadata: el objeto de tipo Metadata para el libro que viene del dispositivo. first_call: True si ésta es la primera ejecución durante la sincronización, False en caso contrario.

class calibre.devices.interface.BookList(oncard, prefix, settings)[fuente]

Bases: list

Una lista de libros. Cada objeto Book debe tener los campos

title (título)

authors (autores)

size (tamaño del archivo del libro)

datetime (tupla de tiempo UTC)

path (ruta de acceso del libro en el dispositivo)

thumbnail (miniatura, puede ser None) es bien un objeto str o bytes con los datos de la imagen, o bien debería tener un atributo image_path que almacena una ruta de acceso completa (en el formato de la plataforma) a la imagen

tags (una lista de textos, puede estar vacía).

supports_collections()[fuente]

Devuelve True si el dispositivo es compatible con colecciones para esta lista de libros.

add_book(book, replace_metadata)[fuente]

Añadir el libro a la lista de libros. El propósito es mantener todos los metadatos internos del dispositivo, devuelve True si la las listas de libros deben sincronizarse

remove_book(book)[fuente]

Elimina un libro de una lista de libros. Simultáneamente, corrige cualquier metadato de dispositivo.

get_collections(collection_attributes)[fuente]

Devuelve un diccionario de colecciones creadas a partirde collection_attributes. Cada entrada del diccionario es de la forma nombre de la colección:[lista de libros]

El listado de los libros se ordena por título, excepto en colecciones creadas a partir de series, donde se usa series_index.

Parámetros:

collection_attributes – Una lista de atributos del objeto Book.

Dispositivos basados en almacenamiento masivo USB

La clase base para tales dispositivos es calibre.devices.usbms.driver.USBMS. Esta clase hereda algunas de sus funcionalidades de sus bases, documentadas a continuación. Un controlador típico basado en USBMS es así:

from calibre.devices.usbms.driver import USBMS

class PDNOVEL(USBMS):
    name = 'Pandigital Novel device interface'
    gui_name = 'PD Novel'
    description = _('Communicate with the Pandigital Novel')
    author = 'Kovid Goyal'
    supported_platforms = ['windows', 'linux', 'osx']
    FORMATS = ['epub', 'pdf']

    VENDOR_ID   = [0x18d1]
    PRODUCT_ID  = [0xb004]
    BCD         = [0x224]

    THUMBNAIL_HEIGHT = 144

    EBOOK_DIR_MAIN = 'eBooks'
    SUPPORTS_SUB_DIRS = False

    def upload_cover(self, path, filename, metadata):
        coverdata = getattr(metadata, 'thumbnail', None)
        if coverdata and coverdata[2]:
            with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
                coverfile.write(coverdata[2])

class calibre.devices.usbms.device.Device(plugin_path)[fuente]

Bases: DeviceConfig, DevicePlugin

Esta clase proporciona una lógina común a todos los controladores para dispositivos que se manifiestan como dispositivos USB de almacenamiento masivo. Proporciona implementaciones para montar o extraer dispositivo USBMS en todas las plataformas.

VENDOR_ID = 0

VENDOR_ID puede ser un entero, una lista de enteros o un diccionario. Si es un diccionario, debe ser un diccionario de diccionarios de la forma:

{
 integer_vendor_id : { product_id : [list of BCDs], ... },
 ...
}

PRODUCT_ID = 0

Un entero o una lista de enteros

BCD = None

BCD puede ser bien None para no distinguir entre dispositivos según BCD, o bien una lista de los números BCD de todos los dispositivos soportados por este controlador.

WINDOWS_MAIN_MEM = None

Texto que identifica la memoria principal del dispositivo en los textos de identificación PnP de Windows. Puede ser None, un texto, una lista de textos o una expresión regular compilada

WINDOWS_CARD_A_MEM = None

Texto que identifica la primera tarjeta del dispositivo en los textos de identificación PnP de Windows. Puede ser None, un texto, una lista de textos o una expresión regular compilada

WINDOWS_CARD_B_MEM = None

Texto que identifica la segunda tarjeta del dispositivo en los textos de identificación PnP de Windows. Puede ser None, un texto, una lista de textos o una expresión regular compilada

OSX_MAIN_MEM_VOL_PAT = None

Usado por la nueva detección de controladores para distinguir la memoria principal de las tarjetas de almacenamiento. Debería ser una expresión regular que corresponda al punto de montaje de la memoria principal asignado por macOS

BACKLOADING_ERROR_MESSAGE = None

MAX_PATH_LEN = 250

La longitud máxima para las rutas de acceso en el dispositivo

NEWS_IN_FOLDER = True

Poner noticias en una carpeta propia

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[fuente]

Parámetros:

key – La clave para desbloquear el dispositivo

log_packets – Si es True, el flujo de paquetes de o al dispositivo se registra

report_progress – Función que se ejecuta con un argumento de progreso en porcentaje (número entre 0 y 100) para diversas tareas. Si el argumento es -1 significa que la tarea no tiene información de progreso.

detected_device – Información de dispositivo desde el examinador de dispositivos

set_progress_reporter(report_progress)[fuente]

Establece una función para mostrar información de progreso.

Parámetros:

report_progress – Función que se ejecuta con un argumento de progreso en porcentaje (número entre 0 y 100) para diversas tareas. Si el argumento es -1 significa que la tarea no tiene información de progreso.

card_prefix(end_session=True)[fuente]

Devuelve una lista de dos elementos con los prefijos para las rutas de acceso en las tarjetas. Si no hay tarjeta, el prefijo correspondiente será None. Por ejemplo: (“/lugar”, “/lugar2”) (None, “lugar2”) (“lugar”, None) (None, None)

total_space(end_session=True)[fuente]

Obtiene el espacio total disponible en los puntos de montaje:

Memoria principal

Tarjeta de almacenamiento A

Tarjeta de almacenamiento B

Devuelve:

Una lista de tres elementos con el espacio total en bytes de (1, 2, 3). Si un dispositivo concreto no tiene alguna de estas ubicaciones, debe devolver 0.

free_space(end_session=True)[fuente]

Obtiene el espacio libre disponible en los puntos de montaje:

Memoria principal

Tarjeta A

Tarjeta B

Devuelve:

Una lista de tres elementos con el espacio libre en bytes de (1, 2, 3). Si un dispositivo concreto no tiene alguna de estas ubicaciones, debe devolver -1.

windows_sort_drives(drives)[fuente]

Ejecutado para distinguir la memoria principal y la tarjeta de almacenamiento para dispositivos en los que WINDOWS_CARD_*_NAME no funciona. Por ejemplo, el EB600

can_handle_windows(usbdevice, debug=False)[fuente]

Método opcional para comprobar si este controlador puede manejar un dispositivo mediante más pruebas. Si no puede, debe devolver False. Este método sólo se ejecuta una vez que los identificadores del fabricante y del producto, así como el BCD se han reconocido, por lo que puede realizar pruebas que requieran un tiempo relativamente prolongado. La implementación predeterminada devuelve True. Este método sólo se ejecuta en Windows. Véase también can_handle().

Tenga en cuenta que para dispositivos basados en USBMS este método de manera predeterminada delega en can_handle(). Así que sólo necesita sustituir can_handle() en la subclase de USBMS.

Parámetros:

usbdevice – Un dispositivo usb devuelto por calibre.devices.winusb.scan_usb_devices()

open(connected_device, library_uuid)[fuente]

Realiza cualquier inicio específico del dispositivo. Se ejecuta una vez que se ha detectado el dispositivo, pero antes de cualquier otra función que comunique con él. Por ejemplo, para dispositivos que se muestran como dispositivos de almacenamiento masivo USB, este método será el responsable de montar el dispositivo o, si se ha montado automáticamente, averiguar dónde. El método calibre.devices.usbms.device.Device.open() tiene una implementación de esta función que puede ser un buen ejemplo para dispositivos de almacenamiento masivo USB.

Este método puede generar una excepción de tipo OpenFeedback para mostrar un mensaje al usuario final.

Parámetros:

connected_device – El dispositivo que se intenta abrir. Es una tupla de (identificador de fabricante, identificador de producto, bcd, nombre de fabricante, nombre de producto, número de serie del dispositivo). Sin embargo, algunos dispositivos no tienen número de serie y en Windows sólo aparecen los tres primeros campos, el resto son None.

library_uuid – El UUID de la biblioteca de calibre actual. Puede ser None si no existe una biblioteca (por ejemplo cuando se usa desde la línea de órdenes).

eject()[fuente]

Desmontar o expulsar el dispositivo del sistema operativo. Esto no comprueba si hay tareas de interfaz gráfica pendientes que tengan que comunicar con el dispositivo.

NOTA: Este método no puede ejecutarse en el mismo subproceso que el resto de los métodos de dispositivo.

post_yank_cleanup()[fuente]

Se ejecuta si el usuario desconecta el dispositivo sin expulsarlo primero.

sanitize_callback(path)[fuente]

Método para permitir a los controladores de dispositivo individuales reemplazar la limpieza de rutas de acceso empleada por create_upload_path().

filename_callback(default, mi)[fuente]

Devuelve la llamada para permitir a los conductores cambiar el nombre de archivo por defecto introducido por create_upload_path()

sanitize_path_components(components)[fuente]

Realiza cualquier limpieza específica del dispositivo en los componentes de la ruta de acceso para los archivos que se copiarán al dispositivo

get_annotations(path_map)[fuente]

Resuelve path_map a annotation_map para archivos en el dispositivo

add_annotation_to_library(db, db_id, annotation)[fuente]

Añadir una anotación a la biblioteca de calibre

class calibre.devices.usbms.cli.CLI[fuente]

class calibre.devices.usbms.driver.USBMS(plugin_path)[fuente]

Bases: CLI, Device

La clase base para todos los dispositivos USBMS. Implementa la lógica para enviar, recibir, actualizar metadatos, retener metadatos, etc.

description = 'Comunicar con un lector de libros electrónicos.'

Una breve descripción de lo que hace este complemento

author = 'John Schember'

El autor de este complemento

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

booklist_class

alias de BookList

book_class

alias de Book

FORMATS = []

Lista ordenada de formatos soportados

CAN_SET_METADATA = []

Especifica si los metadatos de los libros pueden establecerse a través de la interfaz gráfica.

get_device_information(end_session=True)[fuente]

Pregunta al dispositivo por su información interna. Ver L{DeviceInfoQuery}.

Devuelve:

(nombre del dispositivo, versión del dispositivo, versión del software en el dispositivo, tipo MIME) La tupla puede tener un quinto elemento opcional, que es un diccionario de información de unidad. Puede verse un ejemplo en usbms.driver.

set_driveinfo_name(location_code, name)[fuente]

Establece el nombre de dispositivo en el archivo driveinfo como name. Este ajuste persistirá hasta que el archivo se vuelva a crear o se cambie el nombre de nuevo.

Los dispositivos que no son de disco deberían implementar este método según los códigos de ubicación devueltos por el método get_device_information().

books(oncard=None, end_session=True)[fuente]

Devuelve una lista de los libros electrónicos en el dispositivo.

Parámetros:

oncard – Si es “carda” o “cardb”, devuelve una lista de los libros en la tarjeta de almacenamiento especificada, en caso contrario devuelve una lista de los libros en la memoria principal del dispositivo. Si se especifica una tarjeta y no hay libros en ella, devuelva una lista vacía.

Devuelve:

Un objeto BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[fuente]

Copia una lista de libros al dispositivo. Si un archivo ya existe en el dispositivio, deberá ser sustituido. Este método debería generar un error FreeSpaceError si no hay suficiente espacio libre en el dispositivo. El texto del error FreeSpaceError debe contener la palabra «card» si ``on_card` no es None, si no, debe contener la palabra «memory».

Parámetros:

files – Una lista de rutas

names – Una lista de nombres de archivo que los libros deberán tener una vez copiados al dispositivo. len(names) == len(files)

metadata – Si no es None, es una lista de objetos Metadata. La idea es utilizar los metadatos para determinar dónde poner el libro en el dispositivo. len(metadata) == len(files). Aparte de la portada normal (ruta a la portada), también puede haber un atributo de miniatura (thumbnail), que debería tener prioridad. El atributo thumbnail es de la forma (anchura, altura, datos_de_portada como jpeg).

Devuelve:

Una lista de tuplas de 3 elementos. La lista se envía a add_books_to_metadata().

upload_cover(path, filename, metadata, filepath)[fuente]

Envía una portada de libro al dispositivo. La implementación predeterminada no hace nada.

Parámetros:

path – La ruta completa de la carpeta donde se encuentra el libro asociado.

filename – El nombre del archivo del libro electrónico sin la extensión.

metadata – metadatos perteneciente al libro. Use metadata.thumbnail para la portada

filepath – La ruta completa al archivo del libro electrónico

add_books_to_metadata(locations, metadata, booklists)[fuente]

Añade ubicaciones a las listas de libros. Esta función no debe comunicarse con el dispositivo.

Parámetros:

locations – Resultado de una llamada a L{upload_books}

metadata – Lista de objetos Metadata, igual que para upload_books().

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

delete_books(paths, end_session=True)[fuente]

Borrar libros en ubicaciones del dispositivo.

remove_books_from_metadata(paths, booklists)[fuente]

Elimina libros de la lista de metadatos. Esta función no debe comunicarse con el dispositivo.

Parámetros:

paths – rutas a los libros en el dispositivo.

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

sync_booklists(booklists, end_session=True)[fuente]

Actualizar metadatos del dispositivo.

Parámetros:

booklists – Una tupla que contiene el resultado de las llamadas a (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

classmethod normalize_path(path)[fuente]

Devuelve path con los separadores de ruta propios de la plataforma

Acciones de interfaz de usuario

Si añade un complemento propio en un archivo ZIP, debería crear subclases de InterfaceActionBase e InterfaceAction. El método load_actual_plugin() de la nueva subclase de InterfaceActionBase debe devolver un espécimen de la subclase de InterfaceBase.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[fuente]

Bases: QObject

Un complemento que representa una «acción» que puede ejecutarse en la interfaz gráfica. Todos los elementos de la barra de herramientas y de los menús contextuales están implementados mediante estos complementos.

Tenga en cuenta que esta clase es la clase base para estos complementos, pero para integrar el complemento con el sistema de complementos de calibre debe crear una clase envoltorio que haga referencia al complemento real. Ver ejemplos en el módulo calibre.customize.builtins.

Si dos objetos InterfaceAction tienen el mismo nombre, tiene preferencia el que tenga mayor prioridad.

Las subclases deben implementar los métodos genesis(), library_changed(), location_selected(), shutting_down(), initialization_complete() y meth:tag_browser_context_action.

Una vez inicializado, este complemento tiene acceso a la interfaz gráfica principal de calibre vía gui. Puede acceder a otros complementos por su nombre, por ejemplo:

self.gui.iactions['Save To Disk']

Para acceder al complemento real, use el atributo interface_action_base_plugin; este atributo sólo está disponible una vez que el complemento se ha iniciado. Es útil si quiere usasr métodos de la clase del complemento como do_user_config().

La QAction especificada por action_spec se crea automáticamente y está disponible como self.qaction.

name = 'Implement me'

El nombre del complemento. Si dos complementos con el mismo nombre están presentes, el que tenga mayor prioridad tiene preferencia.

priority = 1

La prioridad del complemento. Si dos complementos con el mismo nombre están presentes, el que tenga mayor prioridad tiene preferencia.

popup_type = 1

El tipo de menú emergente para cuando se añade este complemento a una barra de herramientas

auto_repeat = False

Especifica si esta acción debe repetirse automáticamente cuando el atajo de teclado se mantiene presionado.

action_spec = ('text', 'icon', None, None)

De la forma: (texto, ruta_icono, ayuda, atajo_teclado). ruta_icono, ayuda y atajo_teclado pueden ser None. atajo_teclado debe ser un texto, None o una tupla de atajos. Si es None, no se registra ningún atajo de teclado correspondiente a la acción. Si se pasa una tupla vacía, se registra un atajo sin ninguna tecla predefinida.

action_shortcut_name = None

Si no es None, se usa para el nombre que se muestra al usuario al personalizar los atajos de teclado para la acción anterior, en lugar de action_spec[0]

action_add_menu = False

Si es True, se crea automáticamente un menú y se añade a self.qaction

action_menu_clone_qaction = False

Si es True, se añade un clon de self.qaction al menú de self.qaction. Si quiere que el texto de esta acción sea distinto del de self.qaction, establezca esta variable como el nuevo texto.

dont_add_to = frozenset({})

Conjunto de ubicaciones a las que no debe añadirse esta acción. Ver all_locations para una lista de posibles ubicaciones

dont_remove_from = frozenset({})

Conjunto de ubicaciones de las que no se debe eliminar esta acción. Ver: all_locations para obtener una lista de posibles ubicaciones

action_type = 'global'

Tipo de acción. «current» significa que actúa sobre la vista actual, «global» indica una acción que no actúa sobre la vista actual, sino sobre calibre en general

accepts_drops = False

Si es True, este InterfaceAction tendrá la oportunidad de interactuar con acciones de arrastrar y soltar. Ver los métodos accept_enter_event(), :meth`:accept_drag_move_event`, drop_event() para más detalles.

accept_enter_event(event, mime_data)[fuente]

Este método debe devolver True si y sólo si esta acción de interfaz puede gestionar la acción de arrastrar. No ejecute accept o ignore en la acción, la interfaz de calibre se encargará de ello.

accept_drag_move_event(event, mime_data)[fuente]

Este método debe devolver True si y sólo si esta acción de interfaz puede gestionar la acción de arrastrar. No ejecute accept o ignore en la acción, la interfaz de calibre se encargará de ello.

drop_event(event, mime_data)[fuente]

Este método debe realizar algunas acciones útiles y devolver True si y sólo si esta acción de interfaz puede manejar el suceso drop. No ejecute accept o ignore sobre el suceso, la interfaz de calibre se encargará de eso. No debe realizar operaciones largas o que causen bloqueo en esta función. En su lugar lugar emita una señal o use QTimer.singleShot y finalice rápidamente. Vea las acciones predefinidas como ejemplos.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[fuente]

Método práctico para añadir acciones a un QMenu. Devuelve la QAction creada. Esta acción tiene un atributo adicional calibre_shortcut_unique_name que, si no es None, se refiere al nombre único con el que esta acción se registra en el gestor de teclado.

Parámetros:

menu – El QMenu al que se añadirá la nueva acción creada

unique_name – Un nombre único para esta acción. Debe ser un nombre globalmente único, así que hágalo tan descriptivo como sea posible. Si duda, añádale un identificador UUID.

text – El texto de la acción.

icon – Un QIcon o un nombre de archivo. El nombre de archivo se pasa al QIcon.ic() predefinido, así que no necesita pasar la ruta completa a la carpeta de imágenes.

shortcut – Un texto, una lista de textos, None o False. Si es False, no se registra ningún atajo de teclado para esta acción. Si es None, se registra un atajo de teclado sin ninguna tecla predeterminada. Con un texto o lista de textos se registra un atajo con las teclas asociadas predeterminadas que se especifiquen.

description – Una descripción para esta acción. Usado para establecer ayudas de herramienta.

triggered – Un objeto ejecutable conectado con la señal desencadenada de la acción creada.

shortcut_name – El texto que se muestra al usuario al personalizar los atajos de teclado para esta acción. De manera predeterminada toma el valor de text.

persist_shortcut – Los atajos para acciones que no aparecen siempre, o que dependen de la biblioteca, pueden desaparecer al modificar otros atajos a menos que persist_shortcut sea True.

load_resources(names)[fuente]

Si este complemento viene en un archivo en formato ZIP (complemento añadido por el usuario), este método le permitirá cargar recursos desde el archivo ZIP.

Por ejemplo, para cargar una imagen:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

Parámetros:

names – Lista de rutas a los recursos en el archivo ZIP utilizando / como separador

Devuelve:

Un diccionario de la forma {nombre: contenido_del_archivo}. Cualquier nombre que no se encuentre en el archivo ZIP, no estará en el diccionario.

genesis()[fuente]

Configurar este complemento. Sólo se ejecuta una vez durante la inicialización. self.gui está disponible. La acción especificada por action_spec está disponible como self.qaction.

location_selected(loc)[fuente]

Ejecutado siempre que cambia la lista que se muestra en calibre. Actualmente los valores para loc son: library, main, card y cardb.

Este método debe habilitar o deshabilitar esta acción y sus subacciones, según sea adecuado para la ubicación.

library_about_to_change(olddb, db)[fuente]

Ejecutado cada vez que se cambia la biblioteca actual.

Parámetros:

olddb – El objeto LibraryDatabase correspondiente a la biblioteca anterior.

db – El objeto LibraryDatabase correspondiente a la nueva biblioteca.

library_changed(db)[fuente]

Ejecutado cada vez que se cambia la biblioteca actual.

Parámetros:

db – La LibraryDatabase correspondiente a la biblioteca actual.

gui_layout_complete()[fuente]

Ejecutado una vez por acción cuando se completa la disposición de la interfaz gráfica principal. Si la acción necesita hacer cambios en la disposición, deben ocurrir aquí y no en initialization_complete().

initialization_complete()[fuente]

Se ejecuta una vez por acción cuando se completa la inicialización de la interfaz gráfica principal.

tag_browser_context_action(index)[fuente]

Se ejecuta al mostrar el menú contextual en el explorador de etiquetas. index es el QModelIndex que apunta al elemento del explorador de etiquetas sobre el que se pulsó el botón derecho. Puede comprobar su validez con index.valid() y obtener el objeto TagTreeItem correspondiente con index.data(Qt.ItemDataRole.UserRole). Todos los objetos de acción generados por este método se añadirán al menú contextual.

shutting_down()[fuente]

Ejecutado una vez por complemento cuando la interfaz gráfica se está apagando. Libere los recursos en uso, pero procure no bloquear el apagado durante un tiempo prolongado.

class calibre.customize.InterfaceActionBase(*args, **kwargs)[fuente]

Bases: Plugin

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

El autor de este complemento

type = 'Acción de interfaz de usuario'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

can_be_disabled = False

Si es False, el usuario no podrá deshabilitar este complemento. Usar con precaución.

load_actual_plugin(gui)[fuente]

Este método debe devolver el objeto real de acción de interfaz.

Complementos de preferencias

class calibre.customize.PreferencesPlugin(plugin_path)[fuente]

Bases: Plugin

Un complemento que representa un widget mostrado en el cuadro de diálogo Preferencias.

Este complemento posee un solo método importante: create_widget(). Los diversos campos de este complemento controlan cómo se categoriza en la interfaz de usuario.

supported_platforms = ['windows', 'osx', 'linux']

Lista de plataformas con las que este complemento es compatible. Por ejemplo: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

El autor de este complemento

type = 'Preferencias'

El tipo de este complemento. Utilizado para clasificar los complementos en la interfaz gráfica.

can_be_disabled = False

Si es False, el usuario no podrá deshabilitar este complemento. Usar con precaución.

config_widget = None

Ruta de importación al módule que contiene una clase llamada ConfigWidget que implementa ConfigWidgetInterface. Usado por create_widget().

category_order = 100

Donde debe estar la :attr: category de este complemento en la lista de categorías.

name_order = 100

Dónde debe estar el :attr: gui_name de este complemento en la lista de nombres.

category = None

La categoría en la que debe estar este complemento

gui_category = None

El nombre de categoría mostrado al usuario para este complemento

gui_name = None

El nombre que se muestra al usuario para este complemento

icon = None

El icono para este complemento, debe ser una ruta de acceso absoluta

description = None

La descripción usada para ayudas de herramientas y similares

create_widget(parent=None)[fuente]

Crea y devuelve el widget Qt real usado para establecer este grupo de preferencias. El widget debe implementar calibre.gui2.preferences.ConfigWidgetInterface.

La implementación predeterminada usa config_widget para crear el widget.

class calibre.gui2.preferences.ConfigWidgetInterface[fuente]

Esta clase define la interfaz que deben implementar todos los widgets mostrados en el cuadro de diálogo Preferencias. Ver ConfigWidgetBase para una clase base que implementa esta interfaz y también define varios métodos prácticos.

changed_signal = None

Esta señal debe emitirse cuando el usuario cambia un valor en este widget

supports_restoring_to_defaults = True

Es True si y sólo si el método restore_to_defaults() está implementado.

restore_defaults_desc = 'Restablecer las configuraciones a sus valores predeterminados. Debe pulsar «Aplicar» para guardar las configuraciones.'

La ayuda de herramienta para el botón de restauración de los valores predeterminados

restart_critical = False

Si es True el cuadro de diálogo de preferencias no permitirá al usuario establecer más preferencias. Sólo tiene efecto si commit() devuelve True.

genesis(gui)[fuente]

Se ejecuta una vez antes de mostrar el widget, debe realizar cualquier configuración necesaria.

Parámetros:

gui – La interfaz gráfica principal de calibre

initialize()[fuente]

Debe establecer todos los valores de configuración a sus valores iniciales (los valores almacenados en los archivos de configuración). Un sentencia «return» es opcional. Si devuelve «False», el diálogo no se muestra.

restore_defaults()[fuente]

Debe establecer todos los valores de configuración a sus valores predeterminados.

commit()[fuente]

Guarda cualquier configuración modificada. Devuelve True si los cambios requieren un reinicio, False si no es así. Genera una excepción AbortCommit para indicar que ocurrió un error. Es su responsabilidad informar al usuario del error y cómo corregirlo.

refresh_gui(gui)[fuente]

Se ejecuta una vez después de crear este widget. Es responsable de hacer que la interfaz gráfica vuelva a leer cualquier configuración modificada. Tenga en cuenta que la interfaz gráfica predeterminada reinicia varios elementos de todas formas, así que la mayoría de los widgets no necesitarán usar este método.

initial_tab_changed()[fuente]

Se ejecuta si la pestaña mostrada inicialmente cambia antes de que se muestre el widget, pero después de inicializarse.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[fuente]

Clase base que contiene código para añadir widgets de configuración corrientes, como casillas de verificación, cuadros combinados, campos de texto, etc. Ver el método register().

Esta clase gestiona automáticamente, para las configuraciones registradas, notificaciones de cambio, reinicio a valores predeterminados, correspondencia entre objetos de interfaz gráfica y de configuración, etc.

Si el widget de configuración hereda esta clase pero incluye configuraciones que no están registradas, debe reemplazar los métodos de ConfigWidgetInterface y llamar a los métodos de la clase base en los reemplazos.

changed_signal

Esta señal debe emitirse cuando el usuario cambia un valor en este widget

supports_restoring_to_defaults = True

Es True si y sólo si el método restore_to_defaults() está implementado.

restart_critical = False

Si es True el cuadro de diálogo de preferencias no permitirá al usuario establecer más preferencias. Sólo tiene efecto si commit() devuelve True.

register(name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=<class 'calibre.gui2.preferences.Setting'>)[fuente]

Registrar una configuración.

Parámetros:

name – El nombre de la configuración

config_obj – El objeto de configuración que lee y escribe la configuración

gui_name – El nombre del objeto de interfaz gráfica que ofrece una interfaz para modificar la configuración. De manera predeterminada se supone que es 'opt_' + name.

choices – Si esta configuración es de elección múltiple (un cuadro combinado), la lista de posibilidades. La lista es una lista de tuplas de dos elementos de la forma: [(nombre de interfaz, valor), ...].

setting – La clase responsable de la gestión de esta configuración. La clase predeterminada contempla casi todos los casos, por lo que este parámetro es raramente usado.

initialize()[fuente]

Debe establecer todos los valores de configuración a sus valores iniciales (los valores almacenados en los archivos de configuración). Un sentencia «return» es opcional. Si devuelve «False», el diálogo no se muestra.

commit(*args)[fuente]

Guarda cualquier configuración modificada. Devuelve True si los cambios requieren un reinicio, False si no es así. Genera una excepción AbortCommit para indicar que ocurrió un error. Es su responsabilidad informar al usuario del error y cómo corregirlo.

restore_defaults(*args)[fuente]

Debe establecer todos los valores de configuración a sus valores predeterminados.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes

Interfaz de línea de órdenes

Nota

En macOS, las herramientas de línea de órdenes están dentro del paquete calibre, por ejemplo, si ha instalado calibre en /Aplicaciones las herramientas de línea de órdenes están en :file:/Aplicaciones/calibre.app/Contents/MacOS/. Por ejemplo, para ejecutar :file:`ebook-convert se usaría: /Applications/calibre.app/Contents/MacOS/ebook-convert.

Órdenes con documentación

calibre

calibre-customize

calibre-debug

calibre-server

calibre-smtp

calibredb

ebook-convert

ebook-edit

ebook-meta

ebook-polish

ebook-viewer

fetch-ebook-metadata

lrf2lrs

lrfviewer

lrs2lrf

web2disk

Órdenes sin documentación

ebook-device

markdown-calibre

Puede ver la sintaxis de las órdenes sin documentación ejecutándolas sin argumentos en una terminal.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibre

calibre

1calibre [opciones] [ruta_a_libro o url de calibre ...]

Inicia la interfaz gráfica principal de calibre y opcionalmente añade el libro en «ruta_a_libro» a la base de datos. También puede especificar URL de calibre para efectuar otras acciones en lugar de añadir libros. Por ejemplo:

calibre://view-book/biblioteca_de_prueba/1842/epub

abrirá el formato EPUB del libro con identificador 1842 de la biblioteca «biblioteca_de_prueba» en el visor de libros electrónicos de calibre. Los nombres de biblioteca son los nombres de carpeta con los espacios reemplazados por guiones bajos. En el Manual de usuario hay una descripción completa de las distintas acciones basadas en URL.

Cuando alguno de los argumentos de calibre contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--detach

Desacoplar de la terminal en control, si existe (sólo Linux)

--help, -h

mostrar este mensaje de ayuda y terminar

--ignore-plugins

Ignorar los complementos personalizados. Esto es útil si ha instalado algún complemento que impide que calibre arranque normalmente

--no-update-check

No comprobar actualizaciones

--shutdown-running-calibre, -s

Cierra una sesión de calibre que pueda estar abierta. Tenga cuidado, porque si hay tareas en ejecución serán abortadas, úselo con precaución.

--start-in-tray

Iniciar el programa minimizado en la bandeja del sistema.

--verbose, -v

Sin efecto, no utilizar. Presente únicamente por motivos históricos

--version

mostrar el número de versión del programa y terminar

--with-library

Usar la biblioteca situada en la ruta especificada.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibre-customize

calibre-customize

Opciones de calibre-customize

Personalizar calibre cargando complementos externos.

Cuando alguno de los argumentos de calibre-customize contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--add-plugin, -a

Añadir un complemento especificando la ruta del archivo ZIP que lo contiene.

--build-plugin, -b

Para programadores de complementos: Ruta a la carpeta donde está desarrollando el complemento. Esta orden empaquetará automáticamente el complemento y lo actualizará en calibre.

--customize-plugin

Customize plugin. Specify name of plugin and customization string separated by a comma. The customization string is the same as you would enter when customizing the plugin in the main calibre GUI.

--disable-plugin

Desactivar el complemento nombrado

--enable-plugin

Activar el complemento nombrado

--help, -h

mostrar este mensaje de ayuda y terminar

--list-plugins, -l

Listar todos los complementos instalados

--remove-plugin, -r

Eliminar un complemento personalizado por nombre. No tiene efecto en los complementos predefinidos

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibre-debug

calibre-debug

calibre-debug [opciones]

Diversas interfaces de línea de órdenes útiles para depurar calibre. Sin opciones, esta orden inicia un intérprete de Python incrustado. También se pueden iniciar la interfaz gráfica principal de calibre, el visor de libros electrónicos de calibre y el editor de calibre en modo de depuración.

También contiene interfaces para distintos aspectos de calibre que no tienen herramientas de línea de órdenes específicas, como la reducción de caracteres de tipos de letra, la herramienta de comparación de libros, etc.

Se puede usar también calibre-debug para ejecutar scripts independientes. Para ello se usa de esta manera:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

Todo lo que se ponga después de -- se pasará al script. También puede usar calibre-debug como «shebang» en scripts, así:

#!/usr/bin/env -S calibre-debug -e -- –

Cuando alguno de los argumentos de calibre-debug contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--add-simple-plugin

Añadir un complemento sencillo (un complemento que conste de un único archivo .py) especificando la ruta al archivo .py que contiene el código del complemento.

--command, -c

Ejecutar código Python.

--debug-device-driver, -d

Depurar detección de dispositivos

--default-programs

(Des)registrar calibre como programa predeterminado de Windows. --default-programs = (register|unregister)

--diff

Ejecutar la herramienta de comparación de calibre. Por ejemplo: calibre-debug --diff archivo1 archivo2

--edit-book

Ejecutar la herramienta para modificar libros de calibre en modo de depuración.

--exec-file, -e

Ejecutar el código Python en el archivo.

--explode-book, -x

Expandir el libro en la carpeta especificada. Empleo: -x archivo.epub dir_salida Exporta el libro como una colección de archivos HTML y metadatos, que pueden modificarse con las herramientas habituales para HTML. Funciona con archivos EPUB, AZW3, HTMLZ y DOCX.

--export-all-calibre-data

Exportar todos los datos de calibre (libros, configuración, complementos). Normalmente se le pedirá la carpeta donde exportar y las bibliotecas a exportar. También puede especificarlas como argumentos de línea de órdenes para evitar las preguntas. Use rutas absolutas para la carpeta de exportación y las bibliotecas. Puede usarse la palabra clave «all» para exportar todas las bibliotecas. Ejemplos: calibre-debug --export-all-calibre-data # para uso interactivo calibre-debug --export-all-calibre-data /ruta/a/carpeta/de/exportación/vacía /ruta/a/carpeta/de/biblioteca1 /ruta/a/carpeta/de/biblioteca2 calibre-debug --export-all-calibre-data /carpeta/de/exportación all # exportar todas las bibliotecas conocidas

--fix-multiprocessing

Para uso interno

--gui, -g

Ejecutar la interfaz gráfica en modo de depuración. La salida de depuración se imprime en los flujos de salida y error estándar.

--gui-debug

Ejecutar la interfaz gráfica con consola de depuración, con registro en la ruta especificada. Para uso interno, use la opción -g para ejecutar la interfaz gráfica en modo de depuración

--help, -h

mostrar este mensaje de ayuda y terminar

--implode-book, -i

Regenerar un libro previamente expandido. Empleo: -l dir_salida archivo.epub Importa el libro a partir de los archivos en «dir_salida», que debe haber sido creado por una ejecución previa de --explode-book. Asegúrese de especificar el mismo tipo de archivo que se usó al expandir.

--import-calibre-data

Importar datos de calibre previamente exportados

--inspect-mobi, -m

Inspeccionar los archivos MOBI en las rutas especificadas

--paths

Mostrar las rutas necesarias para configurar el entorno de calibre

--run-plugin, -r

Ejecutar un complemento que proporciona una interfaz de línea de órdenes. Por ejemplo: calibre-debug -r "Nombre del complemento" -- archivo1 --option1 Todo lo que aparezca después de -- se pasará al complemento como argumentos.

--run-test, -t

Ejecutar las pruebas nombradas. Usar el valor especial «all» para ejecutar todas las pruebas. Si el nombre de la prueba empieza por un punto se supone que es un nombre de módulo. Si el nombre de la prueba empieza por @ se supone que es un nombre de categoría.

--run-without-debug

No ejecutar con DEBUG activado

--shutdown-running-calibre, -s

Cierra una sesión de calibre que pueda estar abierta. Tenga cuidado, porque si hay tareas en ejecución serán abortadas, úselo con precaución.

--subset-font, -f

Reducir el tipo de letra especificado. Use -- después de esta opción para pasar opciones al programa de reducción de tipos de letra.

--test-build

Probar módulos binarios incluidos

--version

mostrar el número de versión del programa y terminar

--viewer, -w

Ejecutar el visor de libros en modo de depuración

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibre-server

calibre-server

calibre-server [opciones] [ruta de acceso a la carpeta de biblioteca...]

Iniciar el servidor de contenido de calibre. El servidor de contenidos de calibre expone las bibliotecas calibre en Internet. Puede especificar la ruta de acceso a las carpetas de biblioteca como argumentos de calibre-server. Si no especifica ninguna ruta, se utilizarán todas las bibliotecas que el programa principal de calibre conoce.

Cuando alguno de los argumentos de calibre-server contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--access-log

Ruta de acceso para el registro de accesos. Este registro contiene información sobre los clientes que se conectan al servidor y realizan peticiones. De manera predeterminada los accesos no se registran.

--ajax-timeout

Tiempo (en segundos) para esperar una respuesta del servidor al realizar peticiones.

--auth-mode

Elegir el tipo de autenticación usada. Establece el modo de autenticación HTTP usado por el sevidor. Ponga «basic» si tiene el servidor tras un proxy SSL. En caso contrario déjelo en «auto», que usará «basic» si se configura SSL y «digest» si no.

--auto-reload

Volver a cargar automáticamente el servidor cuando cambie el código fuente. Útil para el desarrollo. Debe especificar también un valor pequeño para el tiempo de espera de apagado.

--ban-after

Número de fallos de conexión para bloquear. El número de fallos de conexión tras el que se bloquea una dirección IP

--ban-for

Bloquear direcciones IP que tienen fallos de conexión continuados. Bloquear temporalmente el acceso para las direcciones IP que tienen fallos continuados de conexión durante el número de minutos especificado. Es útil para evitar intentes de adivinar las contraseñas. Si se establece en cero, no se realiza ningún bloqueo.

--book-list-mode

Elegir el modo de lista de libros predeterminado. Establecer el modo de lista de libros predeterminado para nuevos usuarios. Cada usuario puede establecer su propia configuración. El modo predeterminado es el cuadro de portadas.

--compress-min-size

Tamaño mínimo para usar compresión de datos (en bytes).

--custom-list-template

Ruta a un archivo JSON que contiene una plantilla para el modo de lista de libros personalizada. La manera más sencilla de crear tal plantilla es ir a Preferencias > Compartir por la red > Plantilla de lista de libros en calibre, crear una plantilla y exportarla.

--daemonize

Ejecutar el proceso en segundo plano como servicio (sólo Linux).

--displayed-fields

Restringir los campos definidos por el usuario que se muestran. Lista de campos de metadatos definidos por el usuario, separados por comas, que el servidor de contenidos mostrará en las vistas /opds y /mobile. Si especifica esta opción, cualquier campo que no esté en la lista no se mostrará. Por ejemplo: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

Preasignación de sockets, por ejemplo, con activación de sockets systemd. De manera predeterminada esta opción está activada.

--enable-auth, --disable-auth

Autenticación por contraseña para acceder al servidor. Normalmente, el servidor no tiene restricciones, cualquiera puede acceder a él. Puede restringir el acceso a usuarios predefinidos con esta opción. De manera predeterminada esta opción está desactivada.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

Recurrir a la interfaz detectada automáticamente. Si por alguna razón el servidor no se puede asociar a la interfaz especificada en la opción «listen_on», intentará detectar una interfaz que se conecte con el mundo exterior y asociarse a ella. De manera predeterminada esta opción está activada.

--enable-local-write, --disable-local-write

Permitir hacer cambios a las conexiones locales sin autenticación. Normalmente, si no activa la autenticación, el servidor funciona en modo de sólo lectura, para que los usuarios anónimos no puedan hacer cambios en la biblioteca. Esta opción permite hacer cambios a cualquiera que se conecte desde el mismo equipo donde se ejecuta el servidor. Esto es útil si quiere ejecutar el servidor sin autenticación y usar calibredb para hacer cambios en las bibliotecas de calibre. Tenga en cuenta que si activa esta opción permitirá que cualquier programa que se ejecute en el equipo pueda hacer cambios en las bibliotecas de calibre. De manera predeterminada esta opción está desactivada.

--enable-log-not-found, --disable-log-not-found

Registrar peticiones HTTP 404 (No encontrado). Normalmente, el servidor registra todas las peticiones HTTP de recursos que no se encuentran. Esto puede generar gran cantindad de registros inútiles si el servidor es visitado por bots. Use esta opción para desactivarlo. De manera predeterminada esta opción está activada.

--enable-use-bonjour, --disable-use-bonjour

Publicitar OPDS a través de BonJour. Publicitar los canales OPDS a través del servicio BonJour, de manera que las aplicaciones de lectura basadas en OPDS puedan detectar y conectarse al servidor automáticamente. De manera predeterminada esta opción está activada.

--enable-use-sendfile, --disable-use-sendfile

Usar zero-copy en la transferencia de archivos para aumentar el rendimiento. Esto usará transferencias con zero-copy en el núcleo al enviar archivos por la red, lo que aumenta el rendimiento. Sin embargo, puede causar transferencias corruptas en algunos sistemas de archivos defectuosos. Si sufre transferencias corruptas, desactívelo. De manera predeterminada esta opción está activada.

--help, -h

mostrar este mensaje de ayuda y terminar

--ignored-fields

Campos de metadatos definidos por el usuario ignorados. Lista de campos de metadatos definidos por el usuario, separados por comas, que el servidor de contenidos no mostrará en las vistas /opds y /mobile. Por ejemplo: my_rating,my_tags

--listen-on

La interfaz en la que esperar conexiones. De manera predeterminada se esperan conexiones en todas las interfaces IPv6 e IPv4 disponibles. Puede cambiar esto a, por ejemplo, «127.0.0.1» para esperar conexiones sólo del equipo local en IPv4, o a «0.0.0.0» para esperar todas las conexiones IPv4.

--log

Ruta de acceso para el registro del servidor. Este registro contiene información del servidor y errores, no registros de acceso. De manera predeterminada se escribe en stdout (la salida estándar).

--manage-users

Gestionar la base de datos de usuarios con permiso para conectar con este servidor. Puede usarlo en modo automático añadiendo –. Véase calibre-server --manage-users -- help para más detalles. Véase también la opción --userdb.

--max-header-line-size

Tamaño máximo de una cabecera HTTP única (en KB).

--max-job-time

Tiempo máximo para los procesos. Tiempo máximo de ejecución permitido para un proceso (en minutos). Cero significa sin límite.

--max-jobs

Número máximo de procesos. Los procesos se crean según se necesiten y se usan para tareas complejas como preparar un libro para visualizarlo, añadir libros, convertirlos, etc. Normalmente, el número máximo de procesos se basa en el número de núcleos de CPU. Puede controlarlo con esta configuración.

--max-log-size

Tamaño máximo de archivo de registro (en MB). El tamaño máximo de los archivos de registro generados por el servidor. Cuando el registro se hace mayor que este tamaño, se rota automáticamente. Un valor de cero desactiva la rotación de registros.

--max-opds-items

Número máximo de libros en los canales OPDS. El número máximo de libros que el servidor devolverá en un único canal de adquisición OPDS.

--max-opds-ungrouped-items

Nímero máximo de elementos sin agrupar en los canales OPDS. Agrupar elementos en categorías tales como autores o etiquetas por la primera letra cuando hay más de este número de elementos. Un valor de cero desactiva el agrupamiento.

--max-request-body-size

Tamaño máximo permitido para los archivos subidos al servidor (en MB).

--num-per-page

Numero de libros que se mostrarán en una página. El número de libros que se muestran en una sola página en el navegador.

--pidfile

Escribir en PID del proceso en el archivo especificado

--port

El puerto en el que esperar conexiones.

--search-the-net-urls

Ruta a un archivo JSON que contiene URL para la función «Buscar en internet». La manera más sencilla de crear tal archivo es ir a Preferencias > Compartir por la red > Buscar en internet en calibre, crear los URL y exportarlos.

--shutdown-timeout

Tiempo de espera total en segundos para un cierre limpio.

--ssl-certfile

Ruta de acceso al archivo de certificado SSL.

--ssl-keyfile

Ruta de acceso al archivo de clave privada SSL.

--timeout

Tiempo (en segundos) tras el que se cierra una conexión inactiva.

--trusted-ips

Permitir hacer cambios a las conexiones sin autenticación desde direcciones IP específicas. Normalmente, si no activa la autenticación, el servidor funciona en modo de sólo lectura, para que los usuarios anónimos no puedan hacer cambios en la biblioteca. Esta opción permite hacer cambios a cualquiera que se conecte desde las direcciones IP especificadas. Debe ser una lista de direcciones o especificaciones de red separadas por comas. Esto es útil si quiere ejecutar el servidor sin autenticación y usar calibredb para hacer cambios en las bibliotecas de calibre. Tenga en cuenta que si activa esta opción permitirá que cualquiera que se conecte desde las direcciones IP especificadas pueda hacer cambios en las bibliotecas de calibre.

--url-prefix

Un prefijo para añadir a todos los URL. Útil si quiere ejecutar este servidor detrás de un proxy inverso. Por ejemplo, usar /calibre como el prefijo de URL.

--userdb

Ruta de acceso a la base de datos para autenticación. La base de datos es un archivo SQLite. Para crearlo, usar --manage-users. Para más información sobre la administración de usuarios, ver https://manual.calibre-ebook.com/es/server.html#managing-user-accounts-from-the-command-line-only

--version

mostrar el número de versión del programa y terminar

--worker-count

Número de procesos usados para gestionar peticiones.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibre-smtp

calibre-smtp

calibre-smtp [opciones] [de a texto]

Enviar un correo usando el protocolo SMTP. calibre-smtp tiene dos modos de funcionamiento. En el modo de creación se especifica «de», «a» y «texto» y con ellos se crea y envía un mensaje de correo. En el modo de filtro, calibre-smtp lee un mensaje de correo completo del flujo de entrada estándar y lo envía.

«texto» es el cuerpo del mensaje de correo. Si no se especifica «texto», se lee un mensaje de correo completo del flujo de entrada estándar. «de» es la dirección de correo del remitente y «a» es la dirección de correo del destinatario. Cuando se lee un mensaje completo del flujo de entrada estándar, «de» y «a» sólo se usan en la conexión SMTP, las cabeceras del mensaje no se modifican.

Cuando alguno de los argumentos de calibre-smtp contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--fork, -f

Bifurcar el proceso y enviar el mensaje en segundo plano. Si se usa esta opción, debería usarse también --outbox para gestionar los fallos de envío.

--help, -h

mostrar este mensaje de ayuda y terminar

--localhost, -l

Nombre del equipo local. Usado para conectar con el servidor SMTP.

--outbox, -o

Ruta a la carpeta maildir donde almacenar mensajes fallidos.

--timeout, -t

Tiempo máximo de espera para la conexión

--verbose, -v

Dar más detalles

--version

mostrar el número de versión del programa y terminar

COMPOSE MAIL

Opciones para crear un correo. Sin efecto si no se especifica un texto

--attachment, -a

Archivo para adjuntar al correo

--subject, -s

Asunto del correo

SMTP RELAY

Opciones para usar un servidor SMTP intermedio para enviar correos. calibre intentará enviar los correos directamente a no ser que se especifique –relay.

--cafile

Ruta a un archivo de certificados CA concatenados en formato PEM, usado para comprobar el certificado del servidor al usar TLS. De manera predeterminada se usan los certificados CA del sistema.

--dont-verify-server-certificate

No comprobar el certificado del servidor al conectar usando TLS. Éste solía ser el comportamiento predeterminado en las versiones de calibre anteriores a 3.27. Si usa un servidor intermedio con un certificado autofirmado o inválido, puede usar esta opción para restablecer el comportamiento anterior a 3.27

--encryption-method, -e

Método de codificación para usar con el servidor intermedio. Las opciones son TLS, SSL y NONE. El valor predeterminado es TLS. ADVERTENCIA: Usar NONE es muy poco seguro

--password, -p

Contraseña para el servidor intermedio

--port

Puerto para conectarcon el servidor intermedio. De manera predeterminada se usa 465 si el método de codificación es SSL y 25 en caso contrario.

--relay, -r

Un servidor SMTP intermedio para enviar correos.

--username, -u

Nombre de usuario para el servidor intermedio

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

calibredb

calibredb

calibredb orden [opciones] [argumentos]

calibredb es la interfaz de línea de órdenes para la base de datos de calibre. Tiene varias subórdenes que se documentan a continuación.

calibredb puede usarse para manipular una base de datos de calibre especificada por una ruta de acceso o un servidor de contenidos de calibre que esté ejecutándose en el equipo local o a través de internet. Puede iniciar un servidor de contenidos de calibre mediante el programa calibre-server o, en el programa principal de calibre, pulsando Conectar y compartir > Iniciar servidor de contenidos. Puesto que calibredb puede realizar cambios en las bibliotecas de calibre, debe configurar la autenticación en el servidor previamente. Hay dos maneras de hacerlo:

Si tiene pensado conectar sólo con un servidor que se ejecute en el mismo equipo, puede usar simplemente la opción --enable-local-write del servidor de contenidos, para permitir que cualquier programa, incluido calibredb, que se ejecute en el equipo local pueda hacer cambios en los datos de calibre. Al ejecutar el servidor desde el programa principal de calibre, esta opción está en Preferencias > Compartir por la red > Avanzado.

Si quiere activar el acceso remoto por internet, debería configurar cuentas de usuario en el servidor y usar las opciones --username y --password de calibredb para darle acceso. Puede configurar la autenticación de usuarios para calibre-server usando la opción --enable-auth y --manage-users para crear las cuentas de usuario. Si ejecuta el servidor desde el programa principal de calibre, use Preferencias > Compartir por la red > Requerir nombre de usuario y contraseña.

Para conectar con un servidor de contenidos en ejecución, pase el URL del servidor con la opción --with-library, vea la documentación de esta opción para más detalles y ejemplos

OPCIONES GLOBALES

list

add

AÑADIR DE CARPETAS

remove

add_format

remove_format

show_metadata

set_metadata

export

catalog

OPCIONES EPUB

saved_searches

add_custom_column

custom_columns

remove_custom_column

set_custom

restore_database

check_library

list_categories

backup_metadata

clone

embed_metadata

search

fts_index

fts_search

OPCIONES GLOBALES

--help, -h

mostrar este mensaje de ayuda y terminar

--library-path, --with-library

Ruta a la biblioteca de calibre. De manera predeterminada se usa la ruta almacenada en las preferencias. También puede conectar a un servidor de contenidos de calibre para ejecutar acciones sobre bibliotecas remotas. Para ello use un URL de la forma http://servidor:puerto/#id_de_biblioteca; por ejemplo, http://localhost:8080/#mislibros. «id_de_biblioteca» es el identificador de la biblioteca con la que quiere conectar en el servidor de contenidos. Puede usar el valor especial «-» para id_de_biblioteca para obtener una lista de identificadores de biblioteca disponibles en el servidor. Para más detalles sobre cómo configurar el acceso mediante el servidor de contenidos, vea https://manual.calibre-ebook.com/es/generated/es/calibredb.html

--password

Contraseña para conectar con un servidor de contenidos de calibre. Para leer la contraseña de la entrada estándar, use el valor especial: <stdin>. Para leer la contraseña de un archivo, use: <f:/path/to/file> (es decir, <f: seguido de la ruta de acceso completa al fichero y > al final). Los paréntesis angulares son necesarios, no olvide escaparlos o usar las comillas adecuadas en el intérprete de órdenes.

--timeout

El tiempo máximo de espera, en segundos, al conectar con calibre por la red. El valor predeterminado es dos minutos.

--username

Nombre de usuario para conectar con un servidor de contenidos de calibre

--version

mostrar el número de versión del programa y terminar

list

calibredb list [opciones]

Lista de los libros disponibles en la base de datos de calibre.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--ascending

Ordenar los resultados en orden ascendente

--fields, -f

Los campos que se mostrarán cuando se listen los libros en la base de datos. Debe ser una lista de campos separada por comas. Campos disponibles: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Valor predeterminado: title,authors. El campo especial «all» puede usarse para seleccionar todos los campos. Además de los campos predefinidos anteriores, también están disponibles los campos personalizados como *nombre_campo; por ejemplo, para un campo personalizado llamado #puntos, use el nombre: *puntos

--for-machine

Generar salida en formato JSON, que es más apropiado para procesado automático. Se ignorar las opciones de longitud de línea y separador.

--limit

El número máximo de resultados que se mostrará. Valor predeterminado: todos

--line-width, -w

La longitud máxima de las líneas en el archivo de salida. De manera predeterminada se toma la anchura de la pantalla.

--prefix

El prefijo para todas las rutas de archivo. El valor predeterminado es la ruta absoluta de la carpeta de la biblioteca.

--search, -s

Filtrar los resultados según la consulta de búsqueda. Para el formato de la consulta de búsqueda acuda a la documentación relacionada con la búsqueda en el Manual de usuario. De manera predeterminada no se realiza el filtrado.

--separator

El texto usado para separar campos. El valor predeterminado es un espacio.

--sort-by

El campo por el que ordenar los resultados. Puede especificar varios campos separándolos con comas. Campos disponibles: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Valor predeterminado: id

--template

La plantilla a ejecutar si «template» aparece en la lista de campos. Valor predeterminado: None

--template_file, -t

Ruta a un archivo que contiene la plantilla a ejecutar si «template» aparece en la lista de campos. Valor predeterminado: None

--template_heading

Encabezado para la columna de plantilla: Predeterminado: template. Esta opción se ignora si está activa la opción --for-machine

add

calibredb add [opciones] archivo1 archivo2 archivo3 ...

Añadir los archivos especificados a la base de datos. También pueden especificarse carpetas, ver las opciones relativas a carpetas más abajo.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--authors, -a

Establece el autor de los libros añadidos

--automerge, -m

Si se encuentran libros con libros y autores similares, añadir los formatos entrantes (archivos) automáticamente a los registros existentes. El valor «ignore» significa que los formatos duplicados se descartas. El valor «overwrite» significa que los formatos duplicados en la biblioteca se sustituyen por los archivos recién añadidos. El valor «new_record» significa que los formatos duplicados se ponen en un nuevo registro de libro.

--cover, -c

Ruta de la portada para usar para el libro añadido

--duplicates, -d

Añadir los libros a la base de datos aunque ya existan. La comparación se realiza sobre el título y autores de los libros. Tenga en cuenta que la opción --automerge tiene prioridad.

--empty, -e

Añadir un libro vacío (sin formatos)

--identifier, -I

Establecer los identificadores para este libro, por ejemplo -I asin:XXX -I isbn:YYY

--isbn, -i

Establece el ISBN de los libros añadidos

--languages, -l

Una lista de idiomas separados por comas (es mejor usar códigos de idioma ISO639, aunque también se reconocen algunos nombres de idioma)

--series, -s

Establece la serie de los libros añadidos

--series-index, -S

Establece el número de la serie de los libros añadidos

--tags, -T

Establece las etiquetas de los libros añadidos

--title, -t

Establece el título de los libros añadidos

AÑADIR DE CARPETAS

Opciones para controlar la adición de libros de carpetas. De manera predeterminada sólo se añaden los archivos con extensiones de tipos de libro electrónico conocidos.

--add

Un patrón de nombre de archivo (glob), los archivos que coincidan con este patrón se añadirán al buscar archivos en las carpetas, incluso si no son de un tipo de libro electrónico conocido. Se puede especificar varias veces con distintos patrones.

--ignore

Un patrón de nombre de archivo (glob), los archivos que coincidan con este patrón se ignorarán al buscar archivos en las carpetas. Se puede especificar varias veces con distintos patrones. Por ejemplo: *.pdf ignorará todos los archivos PDF

--one-book-per-directory, -1

Asumir que cada directorio tiene un solo libro y que todos los archivos de la carpeta son diferentes formatos del mismo libro

--recurse, -r

Procesar carpetas recursivamente

remove

calibredb remove ID

Eliminar los libros identificados por ID de la base de datos. ID debe ser una lista separada por comas de números de identificación (se pueden obtener números de identificación usando la orden «search»). Por ejemplo, 23,34,57-85 (al especificar un intervalo, el último número del intervalo no está incluido).

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--permanent

No usar la Papelera de reciclaje

add_format

calibredb add_format [opciones] ID archivo_libro

Añadir el libro electrónico archivo_libro a los formatos disponibles para el libro identificado por ID. Se puede obtener el ID usando la orden «search». Si el formato ya existe, será sustituido por el nuevo, a no ser que se use la opción de no sustituir.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--as-extra-data-file

Añadir el archivo como un archivo de datos adicional al libro, no como un formato de libro electrónico

--dont-replace

No sustituir el formato si ya existe

remove_format

calibredb remove_format [options] ID fmt

Eliminar el formato fmt del libro identificado por ID. Se puede obtener el ID usando la orden «search». fmt debe ser una extensión de archivo como LRF, TXT o EPUB. Si el libro no tiene el formato fmt disponible, no hace nada.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

show_metadata

calibredb show_metadata [opciones] ID

Muestra los metadatos almacenados en la base de datos de calibre para el libro identificado por ID. ID es un identificador de la orden «search».

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--as-opf

Imprimir los metadatos en formato OPF (XML)

set_metadata

calibredb set_metadata [opciones] id_de_libro [/ruta/a/metadatos.opf]

Asignar los metadatos del libro identificado por id_de_libro a partir del archivo OPF metadatos.opf. El id_de_libro es un identificador dado por la orden «search». Puede ver en qué consiste el formato OPF usando la opción –as-opf en la orden «show_metadata». También puede asignar metadatos a campos individuales con la opción –field.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--field, -f

El campo a modificar. El formato es nombre_de_campo:valor, por ejemplo: --field tags:etiqueta1,etiqueta2. Use --list-fields para obtener una lista de todos los nombres de campos. Puede especificar esta opción varias veces para asignar varios campos. Nota: para los idiomas debe usar los códigos de idioma ISO639 (p. ej. «es» para español, «fr» para francés, etc.). Para los identificadores, la sintaxis es --field identifiers:isbn:XXXX,doi:YYYYY. Para campos booleanos sí/no, use los valores «true» y «false» o «yes» y «no».

--list-fields, -l

Lista los nombres de campo de metadatos que pueden usarse con la opción --field

export

calibredb export [opciones] ID

Exportar los libros especificados por ID (una lista de identificadores separada por comas) al sistema de archivos. La operación de exportación guarda todos los formatos del libro, su portada y metadatos (en un archivo OPF). También se guardan todos los archivos de datos adicionales asociados con el libro. Se pueden obtener identificación con la orden «search».

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--all

Exportar todos los libros de la base de datos, ignorando el listado de ID.

--dont-asciiize

Hacer que calibre convierta todos los caracteres no ASCII en sus equivalentes ASCII para los nombres de archivo. Esto es útil si se guarda en sistemas de archivos antiguos que no tienen compatibilidad completa con nombres de archivo Unicode. Si se especifica esta opción, se desactiva este comportamiento.

--dont-save-cover

Normalmente, calibre guardará la portada en un archivo separado, junto con los archivos de libro electrónico. Si se especifica esta opción, se desactiva este comportamiento.

--dont-save-extra-files

Guardar todos los archivos de datos asociados con el libro al guardar el libro Si se especifica esta opción, se desactiva este comportamiento.

--dont-update-metadata

Normalmente, calibre actualizará los metadatos en los archivos guardados según los datos presentes en la biblioteca de calibre. Esto hace que el guardado en disco sea más lento. Si se especifica esta opción, se desactiva este comportamiento.

--dont-write-opf

Normalmente, calibre escribirá los metadatos en un archivo OPF separado, junto con los archivos de libro electrónico. Si se especifica esta opción, se desactiva este comportamiento.

--formats

Lista de formatos, separados por comas, para guardar cada libro. De manera predeterminada se guardan todos los formatos disponibles.

--progress

Informe de progreso

--replace-whitespace

Sustituir espacios en blanco con guiones bajos.

--single-dir

Exportar todos los libros en una sola carpeta

--template

La plantilla para controlar el nombre y la estructura de carpetas de los archivos guardados. El valor predeterminado es «{author_sort}/{title}/{title} - {authors}», lo que guardará los libros en una subcarpeta por autor con nombres de archivo que contienen el título y el autor. Las variables de control disponibles son: {author_sort, authors, id, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

El formato para mostrar las fechas. %d: día, %b: mes, %m: número del mes, %Y: año. El valor predeterminado es: %b, %Y

--to-dir

Exportar los libros a la carpeta especificada. El valor predeterminado es .

--to-lowercase

Convertir las rutas a minúsculas.

catalog

calibredb catalog /path/to/destination.(csv|epub|mobi|xml...) [options]

Export a catalog in format specified by path/to/destination extension. Options control how entries are displayed in the generated catalog output. Note that different catalog formats support different sets of options. To see the different options, specify the name of the output file and then the –help option.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--ids, -i

Lista de ID de la base de dato, separados por comas, para incluir en el catálogo. Si se declara, no se usará --search. Valor predeterminado: todos

--search, -s

Filtrar los resultados según la búsqueda. Para el formato de la búsqueda, véase la documentación relativa a búsquedas en el Manual de usuario. Valor predeterminado: sin filtrado

--verbose, -v

Mostrar información de salida detallada. Útil para la depuración

OPCIONES EPUB

--catalog-title

Título del catálogo generado, se usará como título en los metadatos. Valor predeterminado: «My Books» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--cross-reference-authors

Crear referencias cruzadas en la sección Autores para libros con varios autores. Valor predeterminado: 'False' Se aplica a: formatos de salida AZW3, EPUB y MOBI

--debug-pipeline

Guardar la salida de las distintas fases del proceso de conversión en la carpeta especificada. Es útil si no está seguro de en qué etapa del proceso ocurre un fallo. Valor predeterminado: «None» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--exclude-genre

Expresión regular que describe las etiquetas que no se considerarán como géneros. Valor predeterminado: «[.+]|^+$» excluye las etiquetas entre corchetes, por ejemplo «[Project Gutenberg]», y «+» que es la etiqueta pretedeterminada para marcar los libros como leídos. Se aplica a: formatos de salida AZW3, EPUB y MOBI

--exclusion-rules

Especifica las reglas que se usarán para excluir libros del catálogo generado. El modelo para una regla de exclusión es bien ('<nombre de la regla>','Etiquetas','<lista de etiquetas separadas por comas>') o bien ('nombre de la regla','<columna personalizada>','<patrón>'). Por ejemplo: (('Libros almacenados','#estado','Almacenado'),) excluirá cualquier libro con el valor «Almacenado» en la columna personalizada «estado». Si se definen varias reglas, se aplican todas. Valor predeterminado: «(('Catalogs','Tags','Catalog'),)» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-authors

Incluir la sección «Autores» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-descriptions

Incluir una sección «Descripciones» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-genres

Incluir una sección «Géneros» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-recently-added

Incluir una sección «Añadidos recientemente» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-series

Incluir una sección «Series» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--generate-titles

Incluir una sección «Títulos» en el catálogo. Valor predeterminado: «False» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--genre-source-field

Campo de origen para la sección de «Géneros». Valor predeterminado: 'Etiquetas' Se aplica a: formatos de salida AZW3, EPUB y MOBI

--header-note-source-field

Campo personalizado que incluye el texto que se insertará en la cabecera de «Descripción». Valor predeterminado: «» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--merge-comments-rule

#<campo personalizado>:[before|after]:[True|False] para especificar: <campo personalizado> Campo personalizado que contiene las notas para unir con comentarios [before|after] Posición de las notas con respecto a comentarios («before»=antes, «after»=después) [True|False] Si «True», se insertará una línea horizontal entre las notas y comentarios Valor predeterminado: «::» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--output-profile

Especifica el perfil de salida. En algunos casos, se requiere un perfil de salida para optimizar el catálogo para un dispositivo. Por ejemplo, «kindle» o «kindle_dx» crea un índice estructurado con secciones y artículos. Valor predeterminado: «None» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--prefix-rules

Especifica las reglas que se usarán para incluir prefijos que indiquen los libros leídos, libros de la lista de deseos y otros prefijos definidos por el usuario. El modelo para una regla de prefijos es ('<nombre de la regla>','<campo de origen>','<patrón>','<prefijo>'). Si se definen varias reglas, se usará la primera que dé lugar a una coincidencia. Valor predeterminado: «(('Read books','tags','+','✓'),('Wishlist item','tags','Wishlist','×'))» Se aplica a: formatos de salida AZW3, EPUB y MOBI

--preset

Usar una configuración guardada creada con el creador de catálogos gráfico. Una configuración especifica todas las opciones para crear un catálogo. Valor predeterminado: 'None' Se aplica a: formatos de salida AZW3, EPUB, MOBI

--thumb-width

Tamaño preferido (en pulgadas) para las portadas en el catálogo. Rango: 1.0 - 2.0 Valor predeterminado: «1.0» Se aplica a: formatos de salida AZW3, EPUB, MOBI

--use-existing-cover

Sustituir la portada existente al generar el catálogo. Valor predeterminado: 'False' Se aplica a: formatos de salida AZW3, EPUB y MOBI

saved_searches

calibredb saved_searches [opciones] (list|add|remove)

Administrar las búsquedas guardadas en la base de datos. Si intenta añadir una búsqueda con un nombre que ya existe, será reemplazada.

Sintaxis para añadir:

calibredb saved_searches add nombre_de_búsqueda expresión_de_búsqueda

Sintaxis para eliminar:

calibredb saved_searches remove nombre_de_búsqueda

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

add_custom_column

calibredb add_custom_column [opciones] etiqueta nombre tipo_de_dato

Crea una columna personalizada. «etiqueta» es un nombre interno para la columna. No debe contener espacios ni dos puntos. «nombre» es el nombre visible de la columna. «tipo_de_dato» es uno de: bool, comments, composite, datetime, enumeration, float, int, rating, series, text

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--display

Un diccionario de opciones para personalizar cómo se interpretan los datos en esta columna. Es un texto en formato JSON. Para columnas de enumeración use --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" Hay muchas opciones que pueden figurar en la variable «display». Las opciones por tipo de columna son: composite: composite_template, composite_sort, make_category,contains_html, use_decorations datetime: date_format enumeration: enum_values, enum_colors, use_decorations int, float: number_format text: is_names, use_decoration La mejor manera de obtener combinaciones válidas es crear una columna personalizada del tipo apropiado en la interfaz gráfica y luego examinar el archivo OPF creado como copia de seguridad para un libro (asegúrese de que se ha creado un nuevo archivo OPF después de añadir la columna). Verá el texto en formato JSON para la variable «display» de la nueva columna en el archivo OPF.

--is-multiple

Esta columna almacena datos de etiquetas (valores separados por comas). Sólo se aplica si el tipo de dato es texto.

custom_columns

calibredb custom_columns [opciones]

Da una lista de las columnas personalizadas disponibles. Muestra las etiquetas de columna e ID.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--details, -d

Mostrar detalles de cada columna.

remove_custom_column

calibredb remove_custom_column [opciones] etiqueta

Elimina la columna personalizada identificada por «etiqueta». Puede ver las columnas disponibles con la orden «custom_columns»

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--force, -f

No pedir confirmación

set_custom

calibredb set_custom [opciones] columna ID valor

Establecer el valor de una columna personalizada para el libro identificado por ID. Puede obtener una lista de ID con la orden «search». Puede obtener una lista de nombres de columnas personalizadas usando la orden «custom_columns»

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--append, -a

Si la columna almacena valores múltiples, agrega los valores especificados a los existentes en lugar de reemplazarlos.

restore_database

calibredb restore_database [options]

Recupera la base de datos a partir de los metadatos almacenados en los archivos OPF en cada carpeta de la biblioteca de calibre. Esto resulta útil si su archivo metadata.db se ha dañado.

ADVERTENCIA: Esta orden regenera completamente la base de datos. Se perderán todas las búsquedas guardadas, categorías de usuario, controles, configuración de conversión guardada por libro y fórmulas personalizadas. Los metadatos recuperados serán tan precisos como lo sean los archivos OPF.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--really-do-it, -r

Llevar a cabo la recuperación. La orden no se ejecutará a menos que se especifique esta opción.

check_library

calibredb check_library [opciones]

Realiza algunas comprobaciones en el sistema de archivos que contiene la biblioteca. El resultado es invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--csv, -c

Salida a formato CSV

--ignore_extensions, -e

Lista de extensiones que se ignorarán separadas por comas. Valor predeterminado: all

--ignore_names, -n

Lista de nombres que se ignorarán separados por comas. Valor predeterminado: all

--report, -r

Lista de resultados separados por comas Valor predeterminado: all

--vacuum-fts-db

Limpiar la base de datos de búsqueda de texto completo. Esto puede ser muy lento y requerir mucha memoria, dependiendo del tamaño de la base de datos.

list_categories

calibredb list_categories [opciones]

Genera un informe de la información de la categoría en la base de datos. La información es el equivalente a lo que se muestra en el explorador de etiquetas.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--categories, -r

Lista separada por comas de los nombres de consulta de las categorías. Valor predeterminado: all

--csv, -c

Salida a formato CSV

--dialect

El tipo de archivo CSV para crear. Posibilidades: excel, excel-tab, unix

--item_count, -i

Generar en la salida sólo el número de elementos en una categoría en lugar de las veces que aparece por elemento en la categoría

--width, -w

La longitud máxima de las líneas en el archivo de salida. De manera predeterminada se toma la anchura de la pantalla.

backup_metadata

calibredb backup_metadata [opciones]

Hace una copia de respaldo de los metadatos almacenados en la base de datos en ficheros OPF individuales en cada carpeta de libro. Esto se lleva a cabo normalmente de manera automática, pero puede ejecutar esta orden para forzar la regeneración de los ficheros OPF, con la opción –all.

Tenga en cuenta que normalmente no hay necesidad de hacer esto, pues los ficheros OPF se actualizan cada vez que se modifican los metadatos.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--all

Normalmente, esta orden sólo actúa sobre libros que tienen ficheros OPF desactualizados. Esta opción hace que actúe sobre todos los libros.

clone

calibredb clone ruta/a/nueva/biblioteca

Crea un clon de la biblioteca actual. Esto crea una nueva biblioteca vacía que tiene las mismas columnas personalizadas, bibliotecas virtuales y otras configuraciones que la biblioteca actual.

La biblioteca clonada no contendrá ningún libro. Si quiere crear un duplicado completo, incluyendo todos los libros, use simplemente las utilidades del sistema operativo para copiar la carpeta de la biblioteca.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

embed_metadata

calibredb embed_metadata [opciones] ID

Actualizar los metadatos en los archivos de libro almacenados en la biblioteca de calibre a partir de los metadatos en la base de datos de calibre. Normalmente, los metadatos se actualizan únicamente cuando se exportan los libros de calibre, esta orden es útil si quiere actualizar los archivos de origen. Tenga en cuenta que los distintos formatos admiten diferentes tipos de metadatos. Puede usar el valor especial “all” para ID, para actualizar los metadatos de todos los libros. También puede usar varios ID separados por espacios y gamas de ID separados por guiones. Por ejemplo: calibredb embed_metadata 1 2 10-15 23

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--only-formats, -f

Actualizar los metadatos sólo en el formato especificado. Especificar varias veces para múltiples formatos. De manera predeterminada se actualizan todos los formatos.

search

calibredb search [opciones] expresión de búsqueda

Busca la expresión de búsqueda especificada en la biblioteca y devuelve una lista separada por comas de ID de libros que coinciden con la expresión. El formato de la salida puede usarse como entrada par otras órdenes que admiten una lista de ID.

La expresión de búsqueda puede ser cualquiera aceptada por el potente lenguaje de búsquedas de calibre, por ejemplo: calibredb search author:asimov “title:»i robot»”

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--limit, -l

El número máximo de resultados que se devuelven. De manera predeterminada se devuelven todos los resultados.

fts_index

calibredb fts_index [opciones] enable/disable/status/reindex

Controlar el proceso de indexado de búsqueda de texto completo.

enable

Activar el indexado de búsqueda de texto completo para esta biblioteca

disable

Desctivar el indexado de búsqueda de texto completo para esta biblioteca

status

Mostrar el estado actual del indexado

reindex

Reindexar libros individuales o la biblioteca completa. Para reindexar libros individuales, especifique los id de los libros como argumentos adicionales tras la orden reindex. Si no se especifican id de libros se reindexa la biblioteca completa.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--indexing-speed

La velocidad de indexado. Usar rápida para un indexado rápido usando todos los recursos del equipo y lenta para un indexado menos exigente en recursos. Tenga en cuenta que la velocidad se restablece en lenta tras cada ejecución.

--wait-for-completion

Esperar a que se indexen todos los libros, mostrando el progreso cada cierto tiempo

fts_search

calibredb fts_search [opciones] expresión de búsqueda

Realizar una búsqueda de texto completo en toda la biblioteca o en un subconjunto de ella.

Cuando alguno de los argumentos de calibredb contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

--do-not-match-on-related-words

Buscar sólo palabras exactas, no similares. De manera que «correction» no coincidirá con «correcting».

--include-snippets

Incluir fragmentos de texto alrededor de cada coincidencia. Tenga en cuenta que esto hace la búsqueda mucho más lenta.

--indexing-threshold

Qué porción de la biblioteca debe estar indexada para que se permita la búsqueda, como porcentaje. El valor predeterminado es 90

--match-end-marker

El marcador usado para indicar el final de una coincidencia dentro de un fragmento

--match-start-marker

El marcador usado para indicar el inicio de una coincidencia dentro de un fragmento

--output-format

El formato en el que se devuelven los resultados de la búsqueda. Puede ser «text» para texto simple o «json» para salida JSON.

--restrict-to

Restringir los libros en los que se busca, bien sea usando una expresión de búsqueda o una lista de id. Por ejemplo: ids:1,2,3 para restringir por id o search:tag:bla para restringir a libros que tengan la etiqueta bla.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

ebook-convert

ebook-convert

ebook-convert archivo_entrada archivo_salida [opciones]

Convertir un libro electrónico de un formato a otro.

archivo_entrada es la entrada y archivo_salida es la salida. Deben indicarse ambos como los dos primeros argumentos de la orden.

El formato del libro electrónico de salida se define a partir de la extensión de archivo_salida. archivo_salida puede también tener el formato especial .EXT, donde EXT es la extensión del archivo de salida. En este caso, el nombre del archivo de salida se obtiene a partir del nombre del archivo de entrada. Tenga en cuenta que los nombres de archivo no deben comenzar con guión. Por último, si archivo_salida no tiene extensión, se interpreta como una carpeta y se generará un «open eBook» (OEB), formado por un conjunto de archivos HTML, en esa carpeta. Estos archivos son los que normalmente se habrían pasado al complemento de salida.

Tras especificar los archivos de entrada y salida, se puede personalizar la conversión indicando varias opciones. Las opciones disponibles dependen de los tipos de archivo de entrada y salida. Para obtener ayuda sobre estas opciones, especifique los nombres de archivo de entrada y salida y después use la opción -h.

Para una documentación completa del sistema de conversión, véase Conversión de libros

Cuando alguno de los argumentos de ebook-convert contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

Las opciones y sus valores predeterminados cambian según los formatos de entrada y salida, por lo que siempre se debe probar con:

ebook-convert myfile.input_format myfile.output_format -h

A continuación se muestran las opciones comunes a todas las conversiones, seguidas de las opciones específicas para cada formato de entrada y salida.

OPCIONES DE ENTRADA

OPCIONES DE SALIDA

APARIENCIA

PROCESADO HEURÍSTICO

BUSCAR Y SUSTITUIR

DETECCIÓN DE ESTRUCTURA

ÍNDICE

METADATOS

DEPURACIÓN

--help, -h

mostrar este mensaje de ayuda y terminar

--input-profile

Especificar el perfil de entrada. El perfil de entrada provee información al sistema de conversión sobre cómo interpretar los distintos datos del documento de entrada. Por ejemplo, las longitudes dependientes de la resolución (en píxeles). Las opciones son: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

Mostrar una lista de fórmulas predefinidas. Puede crear un libro electrónico a partir de una fórmula predefinida de esta manera: ebook-convert «Nombre de fórmula.recipe» salida.epub

--output-profile

Especificar el perfil de salida. El perfil de salida le dice al sistema de conversión cómo optimizar el documento creado para un dispositivo determinado. En algunos casos, es necesario un perfil de salida para generar documentos que funcionen en ciertos dispositivos, pero no suele ser necesario. Las opciones son:cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, ipad, ipad3, irexdr1000, irexdr800, jetbook5, kindle, kindle_dx, kindle_fire, kindle_oasis, kindle_pw, kindle_pw3, kindle_scribe, kindle_voyage, kobo, msreader, mobipocket, nook, nook_color, nook_hd_plus, pocketbook_inkpad3, pocketbook_lux, pocketbook_hd, pocketbook_900, pocketbook_pro_912, galaxy, sony, sony300, sony900, sony-landscape, sonyt3, tablet, generic_eink, generic_eink_hd, generic_eink_large

--version

mostrar el número de versión del programa y terminar

OPCIONES DE ENTRADA

Opciones para controlar el procesado del archivo de entrada mobi

--input-encoding

Especificar la codificación del documento de entrada. Esta opción tiene preferencia sobre la codificación que pueda declarar el propio documento. Es particularmente útil para documentos que no declaran ninguna codificación, o que lo hacen incorrectamente.

OPCIONES DE SALIDA

Opciones para controlar el procesado de la salida epub

--dont-split-on-page-breaks

Desactivar la división en los saltos de página. Normalmente los archivos de entrada se dividen automáticamente en dos archivos en cada salto de página. Esto genera un libro electrónico que se procesa más rápidamente y con menos recursos. Sin embargo, la división es un proceso lento y, si el archivo de entrada contiene muchos saltos de página, es mejor desactivar la división.

--epub-flatten

Esta opción es necesaria sólo si pretende usar el EPUB con FBReaderJ. Con esta opción se simplificará el sistema de archivos dentro del EPUB, poniendo todos los archivos en el nivel superior.

--epub-inline-toc

Insertar un índice que aparecerá como parte del contenido principal del libro.

--epub-max-image-size

El tamaño máximo de la imagen (ancho x alto). Un valor de none significa que se utiliza el tamaño de pantalla del perfil de salida. Un valor de profile significa que no se especifica ningún tamaño máximo. Por ejemplo, un valor de 100x200 hará que todas las imágenes se redimensionen para que su anchura no sea superior a 100 píxeles y su altura no sea superior a 200 píxeles. Tenga en cuenta que esto sólo afecta al tamaño de los propios archivos de imagen. Cualquier imagen puede tener un tamaño diferente dependiendo del estilo que se le aplique en el documento.

--epub-toc-at-end

Colocar el índice insertado al final del libro en lugar de al principio.

--epub-version

La versión de EPUB del archivo que se genera. EPUB 2 es la más ampliamente compatible, use EPUB 3 sólo si sabe que lo necesita.

--extract-to

Extraer los contenidos del archivo generado EPUB a la carpeta especificada. Los contenidos de la carpeta primero son eliminados, así que tenga cuidado.

--flow-size

Dividir todos los archivos HTML mayores de este tamaño (en kB). Esto es necesario porque algunos lectores ePub no pueden manejar archivos muy grandes. El valor predeterminado de 260KB es el tamaño requerido por Adobe Digital Editions. Establecer en 0 para desactivar la división por tamaño.

--no-default-epub-cover

Normalmente, si el archivo de entrada no tiene portada y no especifica una, se generará una portada con el título, autores, etc. Esta opción desactiva la generación de esta portada.

--no-svg-cover

No usar SVG para la portada del libro. Use esta opción si el EPUB va a usarse en un dispositivo que no soporta SVG, como el iPhone o el JetBook Lite. Sin esta opción, dichos dispositivos mostrarán la portada como una página en blanco.

--preserve-cover-aspect-ratio

Cuando se utilice una portada con un archivo en formato SVG esta opción hace que la portada se escale para cubrir el área disponible de pantalla, pero conserva su relación de aspecto (la relación entre la anchura y la altura). Esto supone que puede haber márgenes blancos a los lados o arriba y abajo de la imagen, pero la imagen no se distorsionará. Sin esta opción la imagen puede distorsionarse ligeramente pero no tendrá margenes en blanco.

--pretty-print

Si se indica, el complemento de salida intentará crear una salida que sea lo más humanamente legible posible. Puede no tener ningún efecto sobre algunos complementos de salida.

--toc-title

Título para cualquier índice generado en el texto.

APARIENCIA

Opciones para controlar el aspecto de la salida

--asciiize

Transliterar caracteres Unicode a la representación ASCII. Tenga cuidado al usar esta opción, ya que reemplazará los caracteres Unicode con ASCII. Por ejemplo, sustituirá «Pelé» por «Pele». Tenga en cuenta también que en los casos en los que existen múltiples representaciones para un carácter determinado (por ejemplo, caracteres compartidos por la escritura china y japonesa) se usará la representación basada en el idioma de la interfaz de calibre.

--base-font-size

Tamaño de letra base en pt. Todos los tamaños de letra en el libro generado se pondrán en relación a este tamaño. Si elige un tamaño mayor, hará que todas las letras de salida sean más grandes, o al contrario. De manera predeterminada, cuando el valor es cero, el tamaño de letra base se decide basándose en el perfil de salida seleccionado.

--change-justification

Cambiar la justificación del texto. El valor «left» hace que el texto justificado en el origen quede alineado a la izquierda (no justificado). El valor «justify» hace que el texto no justificado quede justificado. El valor «original» (el predeterminado) no altera la justificación del archivo de origen. Tenga en cuenta que no todos los formatos de salida admiten justificación.

--disable-font-rescaling

Desactivar el redimensionado de los tamaños de letra.

--embed-all-fonts

Incrustar todos los tipos de letra referenciados en el documento de entrada pero no incrustados todavía. Esta opción buscará en el sistema los tipos de letra y, si se encuentran, se incrustarán. La incrustación sólo funciona si el formato al que se convierte admite tipos de letra incrustados, como EPUB, AZW3, DOCX o PDF. Asegúrese de tener una licencia adecuada para incrustar los tipos de letras usados en el documento.

--embed-font-family

Incrustar el tipo de letra especificado en el libro. Esto establece el tipo de letra «base» utilizado en el libro. Si el documento de entrada especifica sus propios tipos de letra, pueden tener prioridad sobre este tipo de letra base. Puede usar la opción de filtrar estilos para eliminar tipos de letra del documento de entrada. Tenga en cuenta que la incrustación de tipos de letra sólo funciona con algunos formatos de salida, principalmente EPUB, AZW3 y DOCX.

--expand-css

De manera predeterminada, calibre usa una forma abreviada para algunas propiedades CSS como «margin», «padding», «border», etc. Esta opción hace que se use la forma expandida completa en su lugar. Tenga en cuenta que el CSS siempre se expande cuando se generan archivos EPUB con uno de los perfiles de salida para Nook, ya que los lectores Nook no admiten CSS abreviado.

--extra-css

La ruta a una hoja de estilo CSS o CSS en bruto. Esta hoja de estilo CSS se agregará a las reglas de estilo del archivo de origen, por lo que puede usarse para anular dichas reglas.

--filter-css

Una lista de propiedades CSS, separadas por comas, que se eliminarán de todas las reglas de estilo CSS. Esto es útil si hay alguna información de estilo que hace que no se pueda cambiar en algún dispositivo. Por ejemplo: font-family,color,margin-left,margin-right

--font-size-mapping

Correspondencia entre los tamaños de letra de CSS y tamaños en pt. Un ejemplo podría ser 12, 12, 14, 16, 18, 20, 22, 24. Éstas son las correspondencias para los tamaños de xx-small a xx-large, y el último tamaño para letras enormes. El algoritmo para ampliar o reducir el texto emplea estos tamaños para determinar el tamaño de letra de manera inteligente. El comportamiento predeterminado es usar una correspondencia basada en el perfil de salida seleccionado.

--insert-blank-line

Insertar una línea en blanco entre párrafos. No funcionará si el archivo de origen no define párrafos (etiquetas <p> o <div>).

--insert-blank-line-size

Establece la altura de las líneas en blanco que se insertan (en unidades em). La altura de las líneas entre los párrafos será el doble del valor que se introduzca aquí.

--keep-ligatures

Mantener las ligaduras presentes en el documento de entrada. Una ligadura es una forma especial de escribir una secuencia de caracteres como ff, fi, ffl, etc. La mayoría de los lectores no soportan ligaduras en sus tipos de letra predeterminados, por lo que no pueden mostrarlas correctamente. De manera predeterminada, calibre convertirá una ligadura en sus caracteres separados. Por el contrario, seleccionar esta opción las mantendrá.

--line-height

Altura de línea en pt. Controla el espacio entre líneas consecutivas de texto. Sólo se aplica a elementos que no definen su propia altura de línea. En la mayoría de los casos, la opción de altura de línea mínima es más útil. De manera predeterminada no se modifica la altura de línea.

--linearize-tables

Algunos documentos mal diseñados usan tablas para controlar la disposición del texto en la página. Cuando se convierten estos documentos suelen dar lugar a texto que se sale de la página y otros problemas. Esta opción extrae el contenido de las tablas y lo presenta de manera lineal.

--margin-bottom

Establecer el margen inferior en pt. El valor predeterminado es 5.0. Un número negativo desactiva esta opción (se mantendrá el margen existente en el documento original). Nota: Los formatos basados en páginas, como PDF y DOCX tienen sus propias configuraciones de margen que tienen prioridad.

--margin-left

Establecer el margen izquierdo en pt. El valor predeterminado es 5.0. Un número negativo desactiva esta opción (se mantendrá el margen existente en el documento original). Nota: Los formatos basados en páginas, como PDF y DOCX tienen sus propias configuraciones de margen que tienen prioridad.

--margin-right

Establecer el margen derecho en pt. El valor predeterminado es 5.0. Un número negativo desactiva esta opción (se mantendrá el margen existente en el documento original). Nota: Los formatos basados en páginas, como PDF y DOCX tienen sus propias configuraciones de margen que tienen prioridad.

--margin-top

Establecer el margen superior en pt. El valor predeterminado es 5.0. Un número negativo desactiva esta opción (se mantendrá el margen existente en el documento original). Nota: Los formatos basados en páginas, como PDF y DOCX tienen sus propias configuraciones de margen que tienen prioridad.

--minimum-line-height

La altura mínima de la línea, como porcentaje del tamaño de letra calculado para el elemento. calibre se asegurará de que cada elemento tenga esta altura de línea como mínimo, a pesar de lo que indique el documento de entrada. Asignar 0 para desactivar. El valor predeterminado es 120%. Utilice esta opción preferentemente a la especificación directa de la altura de línea, a menos que sepa lo que está haciendo. Por ejemplo, puede conseguir texto «doble espaciado» asignándole un valor de 240.

--remove-paragraph-spacing

Eliminar el espacio entre párrafos. También establece sangrado en cada párrafo de 1,5em. La eliminación del espacio no funciona si el archivo de origen no define párrafos (etiquetas <p> o <div>).

--remove-paragraph-spacing-indent-size

Cuando calibre elimina las líneas en blanco entre párrafos, automáticamente establece una sangría para que se distingan bien los párrafos. Esta opción controla la anchura de esta sangría (en unidades em). Si asigna un valor negativo se usará la sangría especificada en el documento de entrada, es decir, calibre no cambia la sangría.

--smarten-punctuation

Convierte comillas rectas, rayas y puntos suspensivos en sus equivalentes tipográficos correctos. Para más detalles, véase https://daringfireball.net/projects/smartypants.

--subset-embedded-fonts

Reducir caracteres en todos los tipos de letra incrustados. Cada tipo de letra incrustado se recorta para que contenga sólo los caracteres que se usan en el documento. Esto reduce el tamaño de los archivos de tipo de letra. Resulta útil si incrusta un tipo de letra particularmente extenso con muchos caracteres sin usar.

--transform-css-rules

Ruta al archivo que contiene las reglas para transformar los estilos CSS de este libro. La manera más fácil de crear dicho archivo es usar el asistente para creación de reglas en la interfaz gráfica de calibre. Se accede a él en la sección «Apariencia > Transformar estilos» del cuadro de diálogo de conversión. Una vez creadas las reglas, se puede usar el botón «Exportar» para guardarlas en un archivo.

--transform-html-rules

Ruta al archivo que contiene las reglas para transformar el HTML de este libro. La manera más fácil de crear dicho archivo es usar el asistente para creación de reglas en la interfaz gráfica de calibre. Se accede a él en la sección «Apariencia > Transformar HTML» del cuadro de diálogo de conversión. Una vez creadas las reglas, se puede usar el botón «Exportar» para guardarlas en un archivo.

--unsmarten-punctuation

Convertir comillas, raya y puntos suspensivos tipográficos a sus equivalentes más simples.

PROCESADO HEURÍSTICO

Modificar el texto y estructura del documento usando patrones comunes. Desactivado de manera predeterminada. Use –enable-heuristics para activarlo. Las acciones individuales pueden desactivarse con las opciones –disable-*.

--disable-dehyphenate

Analizar las palabras con guión en todo el documento. El propio documento se utiliza como un diccionario para determinar si cada guión debe mantenerse o eliminarse.

--disable-delete-blank-paragraphs

Eliminar los párrafos vacíos del documento cuando existen entre otros párrafos

--disable-fix-indents

Convertir los sangrías creados a partir de varios espacios duros en sangrados en código CSS.

--disable-format-scene-breaks

Los marcadores de salto de escena alineados a la izquierda se centrarán. Los saltos de escena con múltiples líneas en blanco se sustituirán por líneas horizontales.

--disable-italicize-common-cases

Buscar palabras y patrones que denotan cursiva y ponerlos en cursiva.

--disable-markup-chapter-headings

Detectar cabeceras y subcabeceras de capítulos sin formato y convertirlas en etiquetas h2 y h3. Esta configuración no creará un índice, pero se puede utilizar junto con la detección de estructura para crear uno.

--disable-renumber-headings

Busca secuencias de etiquetas <h1> o <h2>. Las etiquetas se renumeran para evitar la división en el medio de una cabecera de capítulo.

--disable-unwrap-lines

Unir líneas basándose en la puntuación y otros indicios de formato.

--enable-heuristics

Activar el procesado heurístico. Esta opción debe estar activada para que se pueda realizar cualquier tipo de procesado heurístico.

--html-unwrap-factor

Escala utilizada para determinar la longitud a la cual una línea debe unirse a otra línea. Los valores válidos son números decimales entre 0 y 1. El valor predeterminado es 0.4, un poco menos de la mitad de la línea. Si sólo unas pocas líneas del documento necesitan unirse, debería reducir el valor

--replace-scene-breaks

Sustituir saltos de escena por el texto especificado. De manera predeterminada se usa el texto existente en el documento de entrada.

BUSCAR Y SUSTITUIR

Modificar el texto y la estructura del documento utilizando patrones definidos por el usuario.

--search-replace

Ruta a un archivo que contiene expresiones regulares de búsqueda y sustitución. El archivo debe contener líneas alternas de expresiones regulares seguidas por patrones de sustitución (que pueden ser líneas en blanco). La expresión regular debe ajustarse a la sintaxis de expresiones regulares de Python y el archivo debe estar codificado como UTF-8.

--sr1-replace

Texto de sustitución para el texto encontrado con sr1-search.

--sr1-search

Patrón de búsqueda (expresión regular) que se sustituirá por sr1-replace.

--sr2-replace

Texto de sustitución para el texto encontrado con sr2-search.

--sr2-search

Patrón de búsqueda (expresión regular) que se sustituirá por sr2-replace.

--sr3-replace

Texto de sustitución para el texto encontrado con sr3-search.

--sr3-search

Patrón de búsqueda (expresión regular) que será sustituida por sr3-replace.

DETECCIÓN DE ESTRUCTURA

Control de autodetección de estructura de documento.

--chapter

Expresión XPath para detectar títulos de capítulo. El comportamiento predeterminado es considerar como títulos de capítulo las etiquetas <h1> o <h2> que contengan las palabras «chapter», «book», «section», «prologue», «epilogue» o «part», así como cualquier etiqueta que tenga class="chapter". La expresión debe dar como resultado una lista de elementos. Para desactivar la detección de capítulos use la expresión «/». Véase el Cursillo de XPath en el Manual de usuario de calibre para obtener más ayuda sobre el uso de esta opción.

--chapter-mark

Especificar cómo marcar los capítulos detectados. Con el valor «pagebreak», se insertará un salto de página antes de cada capítulo; con «rule» se insertará una línea antes de cada capítulo; con «both» se marcarán los capítulos con un salto de página y una línea; con «none» se deshabilitará el marcado de capítulos.

--disable-remove-fake-margins

Algunos documentos especifican los márgenes de página añadiendo márgenes a la izquierda y derecha de cada párrafo, calibre intentará detectar y eliminar estos márgenes. A veces esto puede ocasionar que se eliminen márgenes que deberían mantenerse. En tal caso, puede desactivar la eliminación.

--insert-metadata

Incluir los metadatos al principio del libro. Útil para lectores de libros electrónicos que no manejan los metadatos directamente.

--page-breaks-before

Una expresión XPath. Se insertan saltos de página antes de los elementos especificados. Para desactivarlo use la expresión: /

--prefer-metadata-cover

Usar preferentemente la portada detectada en el archivo de origen en vez de la portada especificada.

--remove-first-image

Eliminar la primera imagen del libro de entrada. Es útil si el documento de entrada tiene una imagen de portada no identificada como tal. En tal caso, si asigna una portada en calibre, el documento de salida tendrá dos imágenes de portada si no activa esta opción.

--start-reading-at

Una expresión XPath para detectar dónde debe comenzar la lectura del documento. Algunos programas de lectura de libros electrónicos (entre ellos el Kindle) usan esta ubicación como la posición desde donde iniciar el libro. Lea el tutorial Xpath en el Manual de usuario de calibre para obtener mayor información acerca de esta función.

ÍNDICE

Controla la generación automática del índice. De manera predeterminada, si el archivo de entrada tiene un Índice, se usará éste en lugar del generado automáticamente.

--duplicate-links-in-toc

Al crear un índice a partir de enlaces en el documento de entrada, permitir entradas duplicadas, es decir, permitir más de una entrada con el mismo texto, siempre que apunten a diferentes partes del texto.

--level1-toc

Expresión XPath que especifica todas las etiquetas que deben añadirse en el primer nivel del índice. Si se indica, tiene prioridad sobre otras formas de autodetección. Pueden encontrarse algunos ejemplos en el Cursillo de XPath en el Manual de usuario de calibre.

--level2-toc

Expresión XPath que especifica todas las etiquetas que deben añadirse en el segundo nivel del índice. Cada entrada se añade bajo la entrada previa del nivel uno. Pueden encontrarse algunos ejemplos en el Cursillo de XPath en el Manual de usuario de calibre.

--level3-toc

Expresión XPath que especifica todas las etiquetas que deben añadirse en el tercer nivel del índice. Cada entrada se añade bajo la entrada previa del nivel dos. Pueden encontrarse algunos ejemplos en el Cursillo de XPath en el Manual de usuario de calibre.

--max-toc-links

Número máximo de enlaces a incluir dentro del índice. El valor 0 desactiva esta opción. Valor predeterminado: 50. Los enlaces sólo se agregan al índice si se deteca un número de capítulos menor que el umbral específicado.

--no-chapters-in-toc

No añadir los capitulos autodetectados al índice.

--toc-filter

Eliminar entradas del índice cuyos títulos se corresponden con la expresión regular especificada. Las entradas marcadas y todas sus ramas son eliminadas.

--toc-threshold

Si se detectan menos capítulos que este número, entonces se añaden enlaces al índice. Valor predeterminado: 6

--use-auto-toc

Normalmente, si el archivo de origen tiene un índice, se usa éste en vez del autogenerado. Con esta opción siempre se usará el autogenerado.

METADATOS

Opciones para asignar metadatos en la salida

--author-sort

Texto que se usará para ordenar por autor.

--authors

Establecer autores. Si hay varios autores deben separarse por «&».

--book-producer

Establecer el productor del libro.

--comments

Establecer la descripción del libro.

--cover

Establecer como portada el archivo o URL especificado

--isbn

Establecer el ISBN del libro.

--language

Establecer el idioma.

--pubdate

Establecer la fecha de publicación (se supone la zona horaria actual, a menos que se especifique otra zona horaria)

--publisher

Establecer la editorial del libro.

--rating

Establecer la valoración. Debe ser un número entre 1 y 5.

--read-metadata-from-opf, --from-opf, -m

Leer metadatos del archivo OPF especificado. Los metadatos leídos de este archivo anularán cualquier metadato que haya en el archivo de origen.

--series

Establecer la serie a la que pertenece el libro.

--series-index

Establecer la posición del libro en esta serie.

--tags

Establecer etiquetas para el libro. Debe ser una lista separada por comas.

--timestamp

Establecer la fecha y hora del libro (ya no se usa)

--title

Establecer el título.

--title-sort

La versión del título que se usará para ordenar.

DEPURACIÓN

Opciones para ayudar con la depuración de la conversión

--debug-pipeline, -d

Guardar la salida de las distintas etapas del proceso de conversión en la carpeta especificada. Útil si no está seguro de en qué punto del proceso de conversión está ocurriendo un error.

--verbose, -v

Nivel de detalles. Emplear varias veces para obtener más detalles. Si se emplea dos veces se mostrarán todos los detalles, con una vez se mostrarán detalles intermedios y si no se emplea los detalles serán mínimos.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

ebook-edit

ebook-edit

ebook-edit [opts] [ruta_al_libro] [nombre_de_archivo_en_el_libro ...]

Ejecutar la herramienta de modificación de libros de calibre. Opcionalmente puede especificar nombres de archivos en el libro que se abrirán automáticamente.

Cuando alguno de los argumentos de ebook-edit contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--detach

Desacoplar de la terminal en control, si existe (sólo Linux)

--help, -h

mostrar este mensaje de ayuda y terminar

--select-text

El texto para seleccionar en el libro cuando se abre para modificar

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

ebook-meta

ebook-meta

ebook-meta archivo_de_libro [opciones]

Leer/escribir metadatos de/en archivos de libro electrónico.

Formatos soportados para leer metadatos: azw, azw1, azw3, azw4, cb7, cbc, cbr, cbz, chm, docx, epub, fb2, fbz, html, htmlz, imp, lit, lrf, lrx, mobi, odt, oebzip, opf, pdb, pdf, pml, pmlz, pobi, prc, rar, rb, rtf, snb, tpz, txt, txtz, updb, zip

Formatos soportados para escribir metadatos: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

Los diferentes tipos de archivo admiten diferentes tipos de metadatos. Si se intentan establecer metadatos en un tipo de archivo que no los soporta, los metadatos se obviarán.

Cuando alguno de los argumentos de ebook-meta contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--author-sort

Texto que se usará para ordenar por autor. Si no se especifica esta opción y sí el autor(es), se generará automáticamente a partir del autor(es).

--authors, -a

Establecer los autores. Si hay varios autores deben separarse por «&». Los nombres de los autores deben estar en el formato Nombre Apellido.

--book-producer, -k

Establecer el productor del libro.

--category

Establecer la categoría del libro.

--comments, -c

Establecer la descripción del libro.

--cover

Establecer el archivo especificado como portada.

--date, -d

Establecer la fecha de publicación.

--from-opf

Leer los metadatos del archivo OPF especificado y usarlos para establecer los metadatos del libro electrónico. Los metadatos que se especifiquen en la línea de órdenes tendrán prioridad sobre los leídos del archivo OPF

--get-cover

Obtener la portada del libro electrónico y guardarla en el archivo especificado.

--help, -h

mostrar este mensaje de ayuda y terminar

--identifier

Establecer los identificadores del libro, puede usarse varias veces. Por ejemplo: --identifier uri:https://acme.com --identifier isbn:12345 Para eliminar un identificador, especifique un valor en blanco: --identifier isbn: Tenga en cuenta que en archivos EPUB, un identificador marcado como identificador del paquete no puede eliminarse.

--index, -i

Establecer la posición del libro en esta serie.

--isbn

Establecer el ISBN del libro.

--language, -l

Establecer el idioma.

--lrf-bookid

Establecer el BookID en archivos LRF

--publisher, -p

Establecer la editorial del libro.

--rating, -r

Establecer la valoración. Debe ser un número entre 1 y 5.

--series, -s

Establecer la serie a la que pertenece el libro.

--tags

Establecer etiquetas para el libro. Debe ser una lista separada por comas.

--title, -t

Establecer el título.

--title-sort

La versión del título que se usara para ordenar. Si no se especifica esta opción y sí el título, se generará automáticamente a partir del título.

--to-opf

Especificar el nombre de un archivo OPF. Los metadatos se escribirán en el archivo OPF.

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

ebook-polish

ebook-polish

ebook-polish [opciones] archivo_de_entrada [archivo_de_salida]

Pulir libros le permite poner un toque de perfección en los libros creados con esmero.

Al pulir se intenta mantener al mínimo los cambios en el código interno del libro. Contrariamente a la conversión, no aplana el código CSS, ni cambia el nombre de ficheros, ni cambia tamaños de letra, ni ajusta los márgenes, etc. Toda acción se realiza con el menor conjunto de cambios necesario para obtener el efecto deseado.

Debería usar esta herramienta como el último paso en el proceso de creación de un libro.

Tenga en cuenta que esta acción sólo funciona con archivos en los formatos AZW3 o EPUB.

Cuando alguno de los argumentos de ebook-polish contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--add-soft-hyphens, -H

Se añaden guiones discrecionales a todas las palabras del libro. Esto permite que el libro se muestre mejor con texto justificado en lectores que no permiten la división de palabras.

--compress-images, -i

Comprimir sin pérdidas las imágenes en el libro para reducir su tamaño sin afectar la calidad de imagen.

--cover, -c

Ruta a la imagen de portada. Cambia la portada especificada en el libro. Si no hay una portada presente o si no está correctamente identificada, inserta una nueva portada.

--embed-fonts, -e

Incrustar todos los tipos de letra referenciados en el documento de entrada pero no incrustados todavía. Esta opción buscará los tipos de letra en el sistema y, si se encuentran, se incrustarán en el documento. Asegúrese de tener una licencia adecuada para incrustar los tipos de letras usados en el documento.

--help, -h

mostrar este mensaje de ayuda y terminar

--jacket, -j

Insertar una página de «sobrecubierta» al principio del libro con todos los metadatos del libro, como título, etiquetas, autores, serie, comentarios, etc. Se sustituirá cualquier sobrecubierta ya existente.

--opf, -o

Ruta a un fichero OPF. Los metadatos del libro se actualizan a partir del fichero OPF.

--remove-jacket

Eliminar una página de sobrecubierta insertada previamente.

--remove-soft-hyphens

Eliminar guiones discrecionales de todo el texto del libro.

--remove-unused-css, -u

Eliminar todas las reglas sin usar de las hojas de estilo y etiquetas <style>. Algunos libros creados a partir de plantillas de producción pueden contener un gran número de reglas CSS innecesarias que no afectan a ningún contenido del libro. Estas reglas adicionales pueden ralentizar los lectores, que necesitan procesarlas todas.

--smarten-punctuation, -p

Convertir rayas, puntos suspensivos, comillas rectas, guiones múltiples, etc. en sus equivalentes tipográficos correctos. Tenga en cuenta que el algoritmo puede dar lugar a resultados incorrectos en algunos casos, especialmente cuando aparecen apóstrofos iniciales en una palabra.

--subset-fonts, -f

Reducir caracteres significa que se eliminan los caracteres sin usar de un tipo de letra incrustado, de manera que sólo se mantienen los caracteres de ese tipo de letra que se usan en el libro. Esto puede reducir considerablemente el tamaño de los archivos de tipo de letra (una reducción del 50% es normal). Por ejemplo, si el libro usa un tipo de letra determinado para las cabeceras, la reducción de caracteres mantendrá sólo los caracteres presentes en las cabeceras del libro. O si un libro incluye las versiones negrita y cursiva de un tipo de letra, pero los textos en negrita o cursiva son infrecuentes o inexistentes, entonces los archivos correspondientes a las versiones negrita y cursiva pueden reducirse a unos pocos caracteres o incluso eliminarse por completo. La única desventaja de reducir los caracteres de los tipos de letra es que si más adelante decide añadir más texto a los libros, el nuevo texto puede no estar cubierto por el tipo de letra reducido.

--upgrade-book, -U

Actualizar el código interno del libro, si es posible. Por ejemplo, actualizar libros EPUB 2 a EPUB 3.

--verbose

Proporciona una salida más detallada, útil para depurar.

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

ebook-viewer

ebook-viewer

ebook-viewer [opciones] archivo

Ver un libro electrónico.

Cuando alguno de los argumentos de ebook-viewer contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--continue

Continuar leyendo el último libro abierto

--detach

Desacoplar de la terminal en control, si existe (sólo Linux)

--force-reload

Volver a cargar todos los libros abiertos

--full-screen, --fullscreen, -f

Si se especifica, la ventana del visor de libros electrónicos tratará de iniciarse a pantalla completa.

--help, -h

mostrar este mensaje de ayuda y terminar

--new-instance

Abrir una nueva ventana de visor incluso cuando la opción para usar una sola ventana de visor está activada

--open-at

Posición en la que abrir el libro especificado. La posición es una ubicación o posición a la que se puede llegar con la función Ir a > Ubicación de los controles del visor. También puede usar la forma «toc:cualquier_cosa» y se abrirá en la posición indicada por la primera entrada del índice que contenga el texto «cualquier_cosa». La forma «toc-href:cualquier_cosa» selecciona el href (destino interno del enlace) de los nodos de índice. La coincidencia es exacta. Si desea seleccionar una parte del texto, use la forma «toc-href-contains:cualquier_cosa». La forma «ref:cualquier_cosa» usará referencias del modo de referencia. La forma «search:cualquier_cosa» busca «cualquier_cosa» después de abrir el libro. La forma «regex:cualquier_cosa» realiza una búsqueda con la expresión regular «cualquier_cosa» después de abrir el libro.

--raise-window

Si se especifica, la ventana del visor de libros electrónicos tratará de colocarse en primer plano cuando se inicie el programa.

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [opciones]

Obtener metadatos de libros de Internet. Debe especificar al menos un título, autor o ISBN.

Cuando alguno de los argumentos de fetch-ebook-metadata contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--allowed-plugin, -p

Especificar el nombre de un complemento de descarga de metadatos para usar. De manera predeterminada se usan todos los complementos de metadatos. Se puede especificar varias para múltiples complementos. Nombres de todos los complementos: Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search

--authors, -a

Autor(es) del libro

--cover, -c

Especificar un nombre de archivo. La portada, si se encuentra, se guardará ahí. Sin esta opción no se descarga ninguna portada.

--help, -h

mostrar este mensaje de ayuda y terminar

--identifier, -I

Identificadores como un ASIN, id de Goodreads, etc. Puede especificarse varias veces para múltiples identificadores. Por ejemplo: --identifier asin:B0082BAJA0

--isbn, -i

ISBN del libro

--opf, -o

Generar los metadatos en formato OPF en lugar de texto legible.

--timeout, -d

Tiempo máximo de espera en segundos. El valor predeterminado es 30

--title, -t

Título del libro

--verbose, -v

Imprimir el registro en la consola (flujo de error estándar)

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

lrf2lrs

lrf2lrs

lrf2lrs libro.lrf

Convierte un archivo LRF en un archivo LRS (XML codificado en UTF-8)

Cuando alguno de los argumentos de lrf2lrs contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--dont-output-resources

No guardar imágenes incrustadas y archivos de tipo de letra al disco

--help, -h

mostrar este mensaje de ayuda y terminar

--output, -o

Archivo LRS de salida

--verbose

Dar más detalles

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

lrfviewer

lrfviewer

lrfviewer [opciones] libro.lrf

Leer el libro LRF libro.lrf

Cuando alguno de los argumentos de lrfviewer contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--disable-hyphenation

Desactivar división de palabras. Debería mejorar significativamente la velocidad del procesado.

--help, -h

mostrar este mensaje de ayuda y terminar

--profile

Analizar el procesador de LRF

--verbose

Imprimir más información sobre el proceso

--version

mostrar el número de versión del programa y terminar

--visual-debug

Activar ayudas visuales para depurar el procesado

--white-background

De manera predeterminada el fondo no es completamente blanco, ya que suele ser menos agresivo a la vista. Use esta opción para hacer el fondo totalmente blanco.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

lrs2lrf

lrs2lrf

lrs2lrf [opciones] archivo.lrs

Compilar un archivo LRS en un archivo LRF.

Cuando alguno de los argumentos de lrs2lrf contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--help, -h

mostrar este mensaje de ayuda y terminar

--lrs

Convierte LRS a LRS, útil para depuración.

--output, -o

Ruta del archivo de salida

--verbose

Procesado detallado

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Interfaz de línea de órdenes »

web2disk

web2disk

web2disk URL

Donde URL es por ejemplo https://google.com

Cuando alguno de los argumentos de web2disk contenga espacios, póngalo entre comillas. Por ejemplo: «/some path/with spaces»

[opciones]

--base-dir, -d

Carpeta base en la cual se almacena el URL. El valor predeterminado es .

--delay

Intervalo mínimo en segundos entre adquisiciones de datos consecutivas. Valor predeterminado: 0 s

--dont-download-stylesheets

No descargar estilos CSS.

--encoding

Codificación para los sitios web que está intentando descargar. De manera predeterminada se intentará averiguar la codificación.

--filter-regexp

Se omitirá cualquier enlace que coincida con esta expresión regular. Esta opción puede especificarse múltiples veces, lo que hará que se omitan los enlaces que coincidan con cualquiera de las expresiones. De manera predeterminada no se omite ningún enlace. Si se especifica tanto un filtro como una coincidencia de expresiones regulares, se aplicará primero el filtro.

--help, -h

mostrar este mensaje de ayuda y terminar

--match-regexp

Sólo se seguirán los enlaces que cumplan la expresión regular. Esta opción se puede usar varias veces, en tal caso, siempre que el elace cumpla alguna de las expresiones regulares, se seguirá el enlace. De manera predeterminada se siguen todos los enlaces.

--max-files, -n

El número máximo de archivos a descargar. Esto se aplica solamente a archivos procedentes de etiquetas <a href>. El valor predeterminado es 9223372036854775807

--max-recursions, -r

Máximo número de niveles de recursión, es decir, profundidad de los enlaces a seguir. Valor predeterminado: 1

--timeout, -t

Tiempo máximo de espera de respuesta del servidor (en segundos). Valor predeterminado: 10.0 s

--verbose

Mostrar información de salida detallada. Útil para la depuración

--version

mostrar el número de versión del programa y terminar

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Configurar un entorno de desarrollo de calibre

Configurar un entorno de desarrollo de calibre

calibre es código completamente abierto, licenciado bajo la GNU GPL v3 [https://www.gnu.org/licenses/gpl.html]. Esto significa que cualquiera es libre de descargar y modificar el programa a su gusto. En esta sección, se mostrará cómo configurar un entorno de desarrollo de calibre en el sistema operativo de su elección. calibre está escrito principalmente en Python [https://www.python.org], con algo de código C/C++ para mejorar la velocidad y la interfaz con el sistema. Tenga en cuenta que calibre requiere al menos Python 3.8.

Índice

Filosofía del diseño

Estructura del código

Obtener el código

Enviar cambios para que se incluyan

Entorno de desarrollo en Windows

Entorno de desarrollo macOS

Entorno de desarrollo Linux

Mantener una instalación «normal» y otra de «desarrollo» de calibre en el mismo equipo

Consejos de depuración

Usar sentencias de impresión

Usar un intérprete de Python interactivo

Usar el depurador de Python como un depurador remoto

Usar el depurador en su IDE de Python favorito

Ejecutar scripts arbitrarios en el entorno Python de calibre

Usar calibre en sus proyectos

Instalación binaria de calibre

Instalación de código fuente sobre Linux

Documentación de la API de varias partes de calibre

Filosofía del diseño

calibre tiene sus raíces en el mundo Unix, lo que significa que su diseño es altamente modular. Los módulos interactúan entre sí a través de interfaces bien definidas. Esto hace que la añadir nuevas característicasy corregir errores en calibre sea muy fácil, lo que resulta en un ritmo frenético de desarrollo. Debido a sus raíces, calibre tiene una interfaz completa de línea de órdenes para todas sus funciones, documentadas en Interfaz de línea de órdenes.

El diseño modular de calibre se manifiesta en los «complementos». Hay un cursillo sobre cómo escribir complementos para calibre. Por ejemplo, agregar soporte para un nuevo dispositivo en calibre implica escribir menos de 100 líneas de código en la forma de un complemento de controlador de dispositivo. Puede examinar los controladores predefinidos [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices]. Del mismo modo, agregar soporte para nuevos conversores de formato implica escribir complementos de formato de entrada o salida. Otro ejemplo de la construcción modular es el sistema de fórmulas para la obtención de noticias. Para más ejemplos de complementos diseñados para agregar funciones a calibre, consulte el índice de complementos [https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767] (en inglés).

Estructura del código

Todo el código Python de calibre está en el paquete calibre. Este paquete contiene los siguientes subpaquetes principales

devices - Todos los controladores de dispositivos. Examite alguno de los controladores predefinidos para hacerse una idea de cómo funcionan.

Para obtener más información, consulte: devices.interface que define la interfaz soportada por los controladores de dispositivos y devices.usbms que define un controlador genérico que se conecta a un dispositivo USBMS. Todos los controladores de dispositivos basados en USBMS en calibre descienden de él.

ebooks - Todo el código de conversión y metadatos de libros electrónicos. Un buen punto de partida es calibre.ebooks.conversion.cli que es el módulo que realiza las funciones de la orden ebook-convert. El proceso de conversión se controla mediante conversion.plumber. El código independiente de formato está todo en ebooks.oeb y el código dependiente de formato está ebooks.format_name.

La lectura, escritura y descarga de metadatos están en ebooks.metadata

La conversión tiene lugar en diferentes etapas, para ver la estructura de estas etapas vea conversión-introduction. Las etapas consisten en un complemento de entrada, diversas transformaciones y un complemento de salida. El código que controla las etapas está en plumber.py. La sucesión de etapas trabaja sobre una representación del libro electrónico que es como un epub descomprimido, con manifiesto, lomo, índice, guía, contenidos html, etc. La clase que maneja esta representación es OEBBook en ebooks.oeb.base. Las diversas transformaciones que se aplican al libro durante las conversiones están en oeb/transforms/*.py. Los complementos de entrada y salida están en conversion/plugins/*.py.

La modificación de libros electrónicos se realiza mediante un objeto contenedor diferente. Está documentado en Documentación de la API para las herramientas de modificación de libros electrónicos.

db - El motor de base de datos. Vea Documentación de la API para la interfaz de la base de datos para más información sobre la interfaz de la biblioteca de calibre.

servidor de contenido: srv es el servidor de contenido de calibre.

gui2 - La interfaz gráfica de usuario (GUI). La inicialización de la GUI ocurre en gui2.main y gui2.ui. El visor de libros electrónicos está en gui2.viewer. El editor de libros electrónicos está en gui2.tweak_book.

Si desea localizar los puntos de entrada de todos los ejecutables de calibre, vea la estructura entry_points en linux.py [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py].

Si necesita ayuda para entender el código, escriba en el foro de desarrollo https://www.mobileread.com/forums/forumdisplay.php?f=240 y probablemente obtendrá ayuda de uno de los muchos desarrolladores de calibre.

Obtener el código

Puede obtener el código fuente de calibre de dos maneras, usando un sistema de control de versiones o descargando directamente un :website_base:`tarball <dist/src>`_.

calibre utiliza Git [https://www.git-scm.com/], un sistema de control de versiones distribuido. Git está disponible en todas las plataformas que soporta calibre. Una vez instalado Git, puede obtener el código fuente de calibre con la orden:

git clone https://github.com/kovidgoyal/calibre.git

En Windows necesitará la ruta completa, que será algo así como C:\Archivos de programa\Git\git.exe.

calibre es un gran proyecto con un extenso historial de control de código fuente, asi que esto puede tomar un tiempo (de 10 minutos a una hora, dependiendo de la velocidad de la conexión a Internet).

Si quiere obtener el código más rápidamente, el código fuente de la última versión está siempre disponible como un :website_base:`archivo <dist/src>`_.

Para actualizar el código de una rama a su versión más reciente, use la orden:

git pull --no-edit

También puede explorar el código en GitHub [https://github.com/kovidgoyal/calibre].

Enviar cambios para que se incluyan

Si sólo va a hacer algunos pequeños cambios, puede hacer los cambios y crear una «directiva de fusión» que luego puede adjuntar a un informe en el registro de errores [https://bugs.launchpad.net/calibre]. Para ello, realice los cambios y después ejecute:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

Esto creará un archivo my-changes en la carpeta actual, simplemente adjúntelo a un informe en el registro de errores [https://bugs.launchpad.net/calibre]. Tenga en cuenta que esto incluirá todos los cambios que haya realizado. Si sólo desea enviar algunos cambios, tendrá que sustituir el origin/master anterior. Para enviar sólo el último cambio, use:

git format-patch HEAD~1 --stdout > my-changes

Para enviar los últimos n cambios, sustituya 1 por n, por ejemplo, para los últimos 3 cambios:

git format-patch HEAD~3 --stdout > my-changes

Tenga cuidado de no incluir uniones cuando use HEAD~n.

Si planea realizar mucho desarrollo en calibre, el mejor método es crear una cuenta en GitHub [https://github.com]. A continuación se muestra una guía básica para la creación de su propia derivación de calibre, de manera que le permitirá presentar una solicitud de colaboración para que se incluya en el repositorio principal de calibre:

Configure git en el equipo como se describe en este artículo: Setup Git [https://help.github.com/articles/set-up-git] (en inglés)

Configure las claves SSH para la autenticación con GitHub, como se describe aquí: Generating SSH keys [https://help.github.com/articles/generating-ssh-keys] (en inglés)

Vaya a https://github.com/kovidgoyal/calibre y pulse en el botón Fork.

En una terminal escriba:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

Sustituya el <username> de arriba por su nombre de usuario en GitHub. Esto hará que la derivación se actualice localmente.

Puede hacer cambios y fijarlos siempre que lo desee. Cuando esté listo para que su trabajo se una con el repositorio principal, escriba:

git push

y vaya a https://github.com/<username>/calibre, pulse en el botón Pull Request para generar una solicitud de contribución que pueda ser incluida.

Puede actualizar la copia local con código del repositorio principal en cualquier momento haciendo:

git pull upstream

También debe mantener un ojo en el foro de desarrollo de calibre [https://www.mobileread.com/forums/forumdisplay.php?f=240]. Antes de hacer cambios importantes, discútalos en el foro o contacte directamente con Kovid (su dirección de correo electrónico está por todo el código fuente).

Entorno de desarrollo en Windows

Nota

También debe obtener el código fuente de calibre por separado como se describe anteriormente.

Instale calibre normalmente, usando el instalador de Windows [https://calibre-ebook.com/download_windows]. A continuación, abra un símbolo del sistema y vaya a la carpeta del código de calibre previamente descargado. Por ejemplo:

cd C:\Users\kovid\work\calibre

calibre es la carpeta que contiene las subcarpetas src y resources.

El siguiente paso es establecer la variable de entorno CALIBRE_DEVELOP_FROM a la ruta absoluta de la carpeta src. Siguiendo el ejemplo anterior, sería C:\Users\Kovid\work\calibre\src. He aquí una breve guía [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] (en inglés) para establecer variables de entorno en Windows.

Una vez que haya establecido la variable de entorno, abra un nuevo símbolo del sistema y compruebe que se estableció correctamente utilizando la orden:

echo %CALIBRE_DEVELOP_FROM%

Al establecer esta variable de entorno, calibre cargará todo su código Python desde la ubicación especificada.

¡Eso es todo! Ya está listo para comenzar a modificar el código de calibre. Por ejemplo, abra el archivo src\calibre\__init__.py en su editor favorito y añada la línea:

print("Hello, world!")

cerca del inicio del archivo. Ahora ejecute la orden calibredb. La primera línea de salida deberá ser Hello, world!.

También puede configurar un entorno de desarrollo de calibre dentro de Microsoft Visual Studio, si lo desea, siguiendo estas instrucciones [https://www.mobileread.com/forums/showthread.php?t=251201] (en inglés).

Entorno de desarrollo macOS

Nota

También debe obtener el código fuente de calibre por separado como se describe anteriormente.

Instale calibre normalmente, usando el archivo .dmg suministrado [https://calibre-ebook.com/download_osx]. A continuación, abra una Terminal y vaya a la carpeta del código de calibre previamente descargado, por ejemplo:

cd /Users/kovid/work/calibre

calibre es la carpeta que contiene las subcarpetas src y resources. Las herramientas de líneas de órdenes de calibre se encuentran en el paquete de la aplicación calibre, en /Applications/calibre.app/Contents/MacOS; debe añadir esta carpeta a la variable de entorno PATH si desea ejecutar estas herramientas cómodamente.

El siguiente paso es crear un script bash que establezca la variable de entorno CALIBRE_DEVELOP_FROM a la ruta absoluta de la carpeta src cuando ejecute calibre en modo de depuración.

Cree un archivo de texto sencillo:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

Guarde este archivo como /usr/local/bin/calibre-develop, después configure los permisos para poder ejecutarlo:

chmod +x /usr/local/bin/calibre-develop

Una vez hecho esto, ejecute:

calibre-develop

Debería aparecerer alguna información de diagnóstico en la ventana de la terminal mientras calibre se inicia, y debería ver un asterisco después del número de versión en la ventana de la interfaz gráfica, lo que indica que se está ejecutando desde el código fuente.

Entorno de desarrollo Linux

Nota

También debe obtener el código fuente de calibre por separado como se describe anteriormente.

calibre está desarrollado principalmente en Linux. Tiene dos opciones para configurar el entorno de desarrollo. Puede instalar el binario de calibre de modo normal y usarlo como un entorno de ejecución para realizar su desarrollo. Este enfoque es similar al que se utiliza en Windows y macOS. Como alternativa, puede instalar calibre a partir del código fuente. Las instrucciones para configurar un entorno de desarrollo a partir del código fuente están en el archivo INSTALL en el árbol de origen. Aquí vamos a tratar el archivo binario como un entorno de ejecución, que es el método recomendado.

Instale calibre, usando el instalador binario [https://calibre-ebook.com/download_linux]. A continuación, abra una terminal y vaya a la carpeta del código de calibre previamente descargado, por ejemplo:

cd /home/kovid/work/calibre

calibre es la carpeta que contiene las subcarpetas src y resources.

El siguiente paso es establecer la variable de entorno CALIBRE_DEVELOP_FROM a la ruta absoluta de la carpeta src. Siguiendo el ejemplo anterior, sería /home/kovid/work/calibre/src. La manera de establecer variables de entorno depende de la distribución de Linux y la shell que esté utilizando.

Nota

Se recomienda usar el instalador binario suministrado en el repositorio oficial. Si insiste en usar un paquete provisto por su distribución, use las variables CALIBRE_PYTHON_PATH y CALIBRE_RESOURCES_PATH.

Una vez que haya establecido la variable de entorno, abra una nueva sesión de terminal y compruebe que se ha establecido correctamente usando esta orden:

echo $CALIBRE_DEVELOP_FROM

Al establecer esta variable de entorno, calibre cargará todo su código Python desde la ubicación especificada.

¡Eso es todo! Ya está listo para empezar a modificar el código de calibre. Por ejemplo, abra el archivo src/calibre/__init__.py en su editor favorito y añada la línea:

print("Hello, world!")

cerca del inicio del archivo. Ahora ejecute la orden calibredb. La primera línea de salida deberá ser Hello, world!.

Mantener una instalación «normal» y otra de «desarrollo» de calibre en el mismo equipo

El árbol del código fuente de calibre es muy estable y rara vez se rompe, pero si siente la necesidad de ejecutar a partir del código fuente en una biblioteca de prueba separada y ejecutar la versión oficial de calibre en la biblioteca habitual, puede lograr esto fácilmente usando archivos .bat o scripts de shell para ejectuar calibre. El siguiente ejemplo muestra cómo hacer esto en Windows utilizando archivos .bat (las instrucciones para otras plataformas son las mismas, sólo tiene que utilizar un script de shell en lugar de un archivo .bat)

Para ejecutar la versión oficial de calibre con la biblioteca habitual:

calibre-normal.bat::.

calibre.exe "--with-library=C:\path\to\everyday\library folder"

calibre-dev.bat::.

set CALIBRE_DEVELOP_FROM=C:\path\to\calibre\checkout\src
calibre.exe "--with-library=C:\path\to\test\library folder"

Consejos de depuración

Python es un lenguaje de programación dinámico con excelentes prestaciones para la introspección. Kovid escribió el código central de calibre sin siquiera utilizar un depurador. Hay varias estrategias para depurar el código de calibre:

Usar sentencias de impresión

Ésta es la forma favorita de Kovid para depurar. Basta con insertar sentencias de impresión en los puntos de interés y ejecutar el programa en la terminal. Por ejemplo, puede iniciar la interfaz gráfica desde la terminal como:

calibre-debug -g

Del mismo modo, se puede iniciar el visor de libros electrónicos como:

calibre-debug -w /path/to/file/to/be/viewed

El editor de libros electrónicos puede iniciarse como:

calibre-debug --edit-book /path/to/be/edited

Usar un intérprete de Python interactivo

Puede insertar las siguientes dos líneas de código para iniciar una sesión interactiva de Python en ese punto:

from calibre import ipython
ipython(locals())

Cuando se ejecuta desde la línea de órdenes, esto iniciará un intérprete interactivo de Python con acceso a todas las variables definidas localmente (variables en el ámbito local). El modo interactivo puede incluso completar con Tab las propiedades de objetos y puede utilizar las diversas funciones de Python para la introspección, como dir(), type(), repr(), etc.

Usar el depurador de Python como un depurador remoto

Puede utilizar el depurador incorporado de Python (pdb) como un depurador remoto desde la línea de órdenes. En primer lugar, inicie el depurador remoto en el punto del código de calibre en el que esté interesado, de esta forma:

from calibre.rpdb import set_trace
set_trace()

A continuación ejecute calibre, ya sea en modo normal o usando una de las órdenes de depuración de calibre descritas en la sección anterior. Una vez que se alcanza el punto del código anterior, calibre se detendrá, esperando a que se conecte el depurador.

Ahora abra una terminal o un símbolo de sistema y utilice el siguiente comando para iniciar la sesión de depuración:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Puede leer acerca de cómo usar el depurador de Python en la documentación de stdlib de Python para el módulo pdb [https://docs.python.org/library/pdb.html#debugger-commands].

Nota

De forma predeterminada, el depurador remoto intentará conectarse con el puerto 4444. Puede cambiarlo, pasando el parámetro de puerto tanto a la función set_trace() como a cli(), de este modo: set_trace(port=1234) y cli(port=1234).

Nota

El depurador de Python no puede manejar múltiples hilos, así que tiene que llamar set_trace una vez por hilo, cada vez con un número de puerto diferente.

Usar el depurador en su IDE de Python favorito

Es posible utilizar el depurador incorporado en su IDE de Python favorito, si éste admite depuración remota. El primer paso es añadir la el src de calibre descargado a PYTHONPATH en el IDE. En otras palabras, la carpeta que habilitó como CALIBRE_DEVELOP_FROM anteriormente también debe estar en el PYTHONPATH del IDE`.

A continuación coloque el módulo depurador remoto del IDE en la subcarpeta src del código fuente de calibre. Añada cualquier código que necesite para iniciar el depurador remoto en calibre en el punto de interés, por ejemplo, en la función main. A continuación, ejecute calibre de modo normal. El IDE ahora debería ser capaz de conectar con el depurador remoto ejecutándose dentro de calibre.

Ejecutar scripts arbitrarios en el entorno Python de calibre

La orden calibre-debug suministra un par de interruptores útiles para ejecutar su propio código, con acceso a los módulos de calibre:

calibre-debug -c "some Python code"

es ideal para probar un pequeño fragmento de código en la línea de órdenes. Funciona de la misma manera que la opción -c del intérprete de Python:

calibre-debug myscript.py

puede utilizarse para ejecutar su propio script de Python. Funciona de la misma manera que pasar el script por el intérprete de Python, excepto que el entorno de calibre está totalmente inicializado, así que puede utilizar todo el código de calibre en el script. Para utilizar argumentos de línea de órdenes con los scripts utilice la forma:

calibre-debug myscript.py -- --option1 arg1

El -- hace que todos los argumentos posteriores sean pasados al script.

Usar calibre en sus proyectos

Es posible usar directamente las funciones y código de calibre en un proyecto de Python. Existen dos maneras de hacer esto:

Instalación binaria de calibre

Si posee una instalación binaria de calibre, puede utilizar el intérprete de Python incluido con calibre, de esta forma:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Instalación de código fuente sobre Linux

Además de usar la técnica anterior, si realiza una instalación de código fuente en Linux, también puede importar directamente calibre de la siguiente manera:

import init_calibre
import calibre

print(calibre.__version__)

Es esencial que importe el módulo init_calibre antes que cualquier otro módulo o paquete de calibre, pues esto configura el intérprete para ejecutar el código de calibre.

Documentación de la API de varias partes de calibre

Documentación de la API para fórmulas

Documentación de la API para complementos

Documentación de la API para la interfaz de la base de datos

Documentación de la API para las herramientas de modificación de libros electrónicos

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Configurar un entorno de desarrollo de calibre »

Documentación de la API para la interfaz de la base de datos

Documentación de la API para la interfaz de la base de datos

Esta API es segura para subprocesos (utiliza un esquema de bloqueo de un escritor, varios lectores). Puede acceder a esta API así:

from calibre.library import db
db = db('Path to calibre library folder').new_api

Si se trata de un complemento de calibre que es parte de la interfaz gráfica principal, se accede a ella de la siguiente forma:

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[fuente]

Una copia en memoria caché del archivo metadata.db de la biblioteca de calibre. Esta clase también sirve como un API multihilo para acceder a la base de datos. La copia en memoria se mantiene en forma normal para maximizar el rendimiento.

SQLite se usa simplemente como una manera de leer y escribir eficientemente metadata.db. Toda la lógica de lectura, clasificación, búsqueda y almacenamiento en la memoria caché de las tablas está desarrallada de nuevo. Esto fue necesario para obtener el máximo rendimiento y flexibilidad.

class EventType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

book_created = 4

Cuando se crea un nuevo registro de libro en la base de datos, con el identificador de libro como único argumento

book_edited = 8

Cuando se modifica un formato de libro, con argumentos: (identificador de libro, formato)

books_removed = 5

Cuando se eliminan libros de la base de datos con la lista de identificadores de libro como único argumento

format_added = 2

Cuando se añade un formato a un libro, con argumentos: (idendificador de libro, formato)

formats_removed = 3

Cuando se eliminan formatos de un libro, con argumentos (mapa de book_id a un conjunto de formatos eliminados del libro)

indexing_progress_changed = 9

Cuando el progreso de indexado cambia

items_removed = 7

Cuando se eliminan elementos como etiquetas o autores de algunos libros. Argumentos: (nombre_de_campo, identificadores de libro afectados, ids de los elemtos eliminados)

items_renamed = 6

Cuando se cambia el nombre de elementos como etiquetas o autores de algunos o todos los libros. Argumentos: (nombre_de campo, identificadores de libro afectados, mapa de id de elemento antiguo a id de elemento nuevo)

metadata_changed = 1

Cuando se modifican metadatos de algunos libros, con argumentos: (nombre del campo modificado, conjunto de identificadores de libro afectados)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[fuente]

Añadir los libros especificados a la biblioteca. Los libros deben ser iterables de 2-tuplas, cada 2-tuplas de la forma (mi, format_map) donde mi es un objeto de metadatos y format_map es un diccionario en la forma {fmt: path_or_stream}, por ejemplo: {'EPUB':'/path/to/file.epub'}.

Devuelve un par de listas: ids, duplicados. ids contiene los identificadores de libros para todos los libros de reciente creación en la base de datos. duplicados contiene el :code: (mi, format_map) para todos los libros que ya existen en la base de datos según la sencilla detección heurística de duplicados utilizada por has_book().

add_custom_book_data(name, val_map, delete_first=False)[fuente]

Añadir datos para name donde val_map es un mapa de book_ids a valores. Si delete_first es True, se eliminarán todos los datos almacenados previamente.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[fuente]

Añadir archivos de datos adicionales

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[fuente]

Añadir un formato al libro especificado. Devuelve True si el formato se añadió con éxito.

Parámetros:

replace – Si es True, sustituye el formato existente; en caso contrario, si el formato ya existe, devuelve False.

run_hooks – Si es True, los complementos de tipo de archivo se ejecutarán sobre el formato antes y después de ser añadido.

dbapi – Solo para uso interno.

add_listener(event_callback_function, check_already_added=False)[fuente]

Registra una función que se ejecutará después de ciertas acciones sobre esta base de datos. La función debe aceptar tres argumentos (EventType, id_de_biblioteca, datos_específicos_de_EventType)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[fuente]

Añade el recurso especificado para que pueda ser referenciado por notas y devuelve el código de comprobación de su contenido

all_book_ids(type=<class 'frozenset'>)[fuente]

Conjunto fijo de todos los identificadores de libros conocidos.

all_field_for(field, book_ids, default_value=None)[fuente]

Lo mismo que field_for, excepto que opera sobre múltiples libros a la vez.

all_field_ids(name)[fuente]

Conjunto fijo de identificadores para todos los valores del campo name.

all_field_names(field)[fuente]

Conjunto fijo de todos los nombres de campos (sólo se debe utilizar para los campos de tipo varios-uno y varios-varios)

author_data(author_ids=None)[fuente]

Devuelve los datos del autor como un diccionario con claves: name, sort, link

Si no se encuentran autores con los identificadores especificados, se devuelve un diccionario vacío. Si author_ids es None, se devuelven datos para todos los autores.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[fuente]

Dada una lista de autores, devolverá el texto author_sort para los autores, prefiriendo el orden de autor asociado con el autor sobre el texto calculado automáticamente.

books_for_field(name, item_id)[fuente]

Devuelve todos los libros relacionados con el elemento identificado por item_id, donde el elemento pertenece al campo name.

El valor devuelto es un conjunto de identificadores de libros, o el conjunto vacío si el elemento o el campo no existen.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[fuente]

Devuelve el conjunto de libros en la biblioteca virtual especificada

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[fuente]

Comprimir las imágenes de portada para los libros especificados. Una calidad de compresión de 100 realizará una compresión sin pérdidas, en caso contrario será con pérdidas.

La función progress_callback se ejecutará con book_id y los tamaños original y nuevo para cada libro que se procese. Si ocurre un error, el tamaño nuevo será un texto con los detalles del error.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[fuente]

Copia la portada al objeto de tipo archivo dest. Devuelve False si no existe portada o dest es el mismo archivo que la portada actual. dest también puede ser una ruta de acceso, en cuyo caso la portada se copia en la ruta si y sólo si la ruta es diferente de la actual ruta de acceso (considerando la posible distinción entre mayúsculas y minúsculas).

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[fuente]

Copiar el formato fmt al objeto de tipo archivo dest. Si el formato especificado no existe, se presenta un error NoSuchFormat. dest también puede ser una ruta de acceso (a un archivo), en cuyo caso el formato se copia en la ruta si y sólo si la ruta es diferente de la ruta de acceso actual (considerando la posible distinción entre mayúsculas y minúsculas).

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[fuente]

Devuelve la imagen de portada o None. De manera predeterminada, devuelve la portada como una serie de bytes.

ADVERTENCIA: Si se usa as_path, se copia la portada a un archivo temporal y se devuelve la ruta de acceso al archivo temporal. Deberá eliminar el archivo temporal cuando haya terminado con él.

Parámetros:

as_file – Si es True devuelve la imagen como un objeto de archivo abierto (un SpooledTemporaryFile)

as_image – Si es True devuelve la imagen como un objeto QImage

as_pixmap – Si es True devuelve la imagen como un objeto QPixmap

as_path – Si es True devuelve la imagen como una ruta de acceso a un archivo temporal

data_for_find_identical_books()[fuente]

Devuelve datos que pueden usarse para implementar find_identical_books() en un proceso de trabajo sin acceso a la base de datos. Véase db.utils para una implementación.

data_for_has_book()[fuente]

Devuelve datos adecuados para has_book(). Puede usarse para implementar has_book() en un proceso de trabajo sin acceso a la base de datos.

delete_custom_book_data(name, book_ids=())[fuente]

Borra los datos para name. De manera predeterminada se borran todos los datos, si sólo desea borrar datos de ciertos identificadores de libros, señálelos como una lista de identificadores de libros.

delete_trash_entry(book_id, category)[fuente]

Eliminar una entrada de la papelera de reciclaje. Aquí category es “b” para libros y “f” para formatos.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[fuente]

Actualizar metadatos en todos los formatos de los book_ids especificados a los metadatos actuales de la base de datos.

expire_old_trash()[fuente]

Caducar entradas de la papelera de reciclaje que son demasiado antiguas

export_note(field, item_id) → str[fuente]

Exporta la nota como un único documento HTML con imágenes empotradas como URL de tipo data:

fast_field_for(field_obj, book_id, default_value=None)[fuente]

Igual que field_for, salvo que se evita la consulta adicional para obtener el objeto de campo

field_for(name, book_id, default_value=None)[fuente]

Devuelve el valor del campo name para el libro identificado por book_id. Si no existe tal libro o no tiene un valor definido para el campo name o no existe dicho campo, entonces devuelve default_value.

default_value no se utiliza para title, title_sort, authors, author_sort y series_index. Esto se debe a que éstos siempre tienen valores en la base de datos. default_value se utiliza para todas las columnas personalizadas.

El valor devuelto para los campos is_multiple es siempre una tupla, incluso cuando no se encuentran valores (en otras palabras, default_value no tiene efecto). La excepción son los identificadores para los que el valor devuelto es siempre un diccionario. Las tuplas devueltas son siempre en orden de enlace, es decir, el orden en el que fueron creadas.

field_ids_for(name, book_id)[fuente]

Devuelve los identificadores (como una tupla) de los valores que el campo name tiene en el libro identificado por book_id. Si no hay valores, o tal libro o tal campo, se devuelve una tupla vacía.

field_supports_notes(field=None) → bool[fuente]

Devuelve True si y sólo si el campo especificado admite notas. Si field es None, devuelve un conjunto de todos los campos que admiten notas.

find_identical_books(mi, search_restriction='', book_ids=None)[fuente]

Encuentra libros que contienen los autores en «mi» y tienen el mismo título (la búsqueda de título es difusa). Véase también

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[fuente]

Devuelve el formato de libro electrónico como una serie de bytes o None si el formato no existe, o no hay permiso para escribir en el archivo de libro electrónico.

Parámetros:

as_file – Si es True el formato de libro electrónico se devuelve como un objeto de archivo. Tenga en cuenta que el objeto es un archivo SpooledTemporaryFile, por lo que si lo que quiere hacer es copiar el formato a otro archivo, utilice copy_format_to() en su lugar para mejorar el rendimiento.

as_path – Copia el archivo de formato a un archivo temporal y devuelve la ruta de acceso al archivo temporal.

preserve_filename – Si es True y devuelve una ruta de acceso, el nombre de archivo es el mismo que el utilizado en la biblioteca. Tenga en cuenta que, al utilizar este método, llamadas repetidas producen el mismo archivo temporal (que se vuelve a crear cada vez)

format_abspath(book_id, fmt)[fuente]

Devuelve la ruta de acceso absoluta al archivo de libro electrónico de formato format. Casi nunca debe usarlo, ya que rompe la filosofía multihilo de esta API. Use copy_format_to().

Actualmente sólo se usa en calibredb list, el visor, modificar libro, compare_format con formato original, abrir con, modificación de metadatos en masa y los catálogos (vía get_data_as_dict()).

Aparte del visor, abrir con y el editor de libros, no creo que ningún otro realice ninguna operación de entrada o salida con el resultado de esta función.

format_hash(book_id, fmt)[fuente]

Devuelve el código de comprobación para el formato y libro especificados. El tipo de código de comprobación es dependiente del motor, pero generalmente es SHA-256.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[fuente]

Devuelve la ruta, el tamaño y mtime para el formato especificado del libro especificado. No debe utilizar la ruta a menos que sea absolutamente necesario, ya que el acceso directo rompe las garantías multihilo de esta API. Use método copy_format_to() en su lugar.

Parámetros:

allow_cache – Si es True se usan los valores almacenados en memoria, en caso contrario se produce un acceso lento al sistema de archivos. Los valores en la memoría podrían estar desfasados si se ha realizado algún acceso al sistema de archivos fuera de esta API.

update_db – Si es True el campo max_size de la base de datos se actualiza para este libro.

formats(book_id, verify_formats=True)[fuente]

Devuelve una tupla con todos los formatos del libro especificado. Si verify_formats es True, se verifica que los archivos existen en el disco.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][fuente]

Devuelve todos los item_id para los elementos que tienen notas en el campo especificado, o en todos los campos si field_name es None

get_all_link_maps_for_book(book_id)[fuente]

Devuelve todos los enlaces para todos los campos referenciados por el libro identificado por book_id. Si book_id no existe, el método devuelve {}.

Ejemplo: Suponiendo que el autor A tiene el enlace X, el autor B tiene el enlace Y, la etiqueta S tiene el enlace F y la etiqueta T tiene el enlace G. Si el libro 1 tiene autor A y etiqueta T, este método devuelve {'authors':{'A':'X'}, 'tags':{'T', 'G'}}. Si el autor del libro 2 no es ni A ni B y no tiene etiquetas, este método devuelve {}.

Parámetros:

book_id – el identificador del libro en cuestión.

Devuelve:

{campo: {valor_campo, valor_enlace}, ...} para todos los campos con un valor_campo que contenga un valor del enlace no vacío para ese libro

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[fuente]

Usado internamente para implementer el exlorador de etiquetas

get_custom_book_data(name, book_ids=(), default=None)[fuente]

Obtener datos para name. De manera predeterminada devuelve datos para todos los book_ids, pase una lista de identificadores de libros si sólo desea algunos dato. Devuelve un mapa de book_id a valores. Si un valor particular no puede ser descodificado, utiliza el predeterminado.

get_id_map(field)[fuente]

Devuelve un mapa de números de identificación a valores para el campo especificado. El campo debe ser un campo de varios-uno o varios-varios, de lo contrario se presenta un ValueError.

get_ids_for_custom_book_data(name)[fuente]

Devuelve el conjunto de identificadores de libros para los que name tiene datos.

get_item_id(field, item_name)[fuente]

Devuelve el identificador del elemento para item_name (no distingue mayúsculas y minúsculas) o None si no se encuentra

get_item_ids(field, item_names)[fuente]

Devuelve el identificador del elemento para item_name (no distingue mayúsculas y minúsculas)

get_item_name(field, item_id)[fuente]

Devuelve el nombre del elemento para el elemento especificado por item_id en el campo especificado. Ver también get_id_map().

get_item_name_map(field, normalize_func=None)[fuente]

Devuelve una correspondencia entre valores de elemento e id

get_link_map(for_field)[fuente]

Devuelve un diccionario de enlaces para el campo proporcionado.

Parámetros:

for_field – el nombre de búsqueda del campo para el que se quiere el mapa de enlaces

Devuelve:

{valor_campo:valor_enlace, ...} para los enlaces no vacíos

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[fuente]

Devuelve metadatos para el libro identificado por book_id como un objeto de calibre.ebooks.metadata.book.base.Metadata. Tenga en cuenta que la lista de formatos no se verifica. Si get_cover es True, se devuelve la portada, ya sea como una ruta de acceso a un archivo temporal o como mi.cover, o si cover_as_data es True como mi.cover_data.

get_next_series_num_for(series, field='series', current_indices=False)[fuente]

Devuelva el siguiente índice la serie especificada, teniendo en cuenta las distintas preferencias que controlan la generación del siguiente número de serie.

Parámetros:

field – El campo de tipo serie (de manera predeterminada la columna predefinida «series»)

current_indices – Si es True, devuelve un mapa de book_id al valor actual de series_index.

get_notes_resource(resource_hash) → dict | None[fuente]

Devuelve un diccionario que contiene los datos y el nombre del recurso o None si no se encuentra ningún recurso con el código de comprobación (hash) especificado

get_proxy_metadata(book_id)[fuente]

Como get_metadata() excepto que devuelve un objeto ProxyMetadata que sólo lee valores desde la base de datos bajo demanda. Es mucho más veloz que get_metadata() cuando sólo se quiere acceder a un pequeño número de campos desde el objeto metatados devuelto.

get_usage_count_by_id(field)[fuente]

Devuelve un mapa de identificador a cuenta de uso para todos los valores del campo especificado, que debe ser un campo de tipo varios-uno o varios-varios.

has_book(mi)[fuente]

Devuelve True si y sólo si la base de datos contiene una entrada con el mismo título que el objeto Metadata pasado como argumento. La comparación no distingue mayúsculas y minúsculas. Véase también

has_format(book_id, fmt)[fuente]

Devuelve True si y sólo si el formato existe en el disco.

has_id(book_id)[fuente]

Devuelve True si y sólo si el book_id especificado existe en la base de datos.

import_note(field, item_id, path_to_html_file, path_is_data=False)[fuente]

Importa una nota previamente exportada o un archivo HTML arbitrario como la nota para el elemento especificado

init()[fuente]

Inicializar esta copia en caché con datos del motor.

items_with_notes_in_book(book_id: int) → dict[str, dict[int, str]][fuente]

Devuelve un diccionario de campo y elementos que tienen notas asociadas para ese campo en el libro especificado

link_for(field, item_id)[fuente]

Devuelve el enlace, si lo hay, para el elemento especificado o None si no se encuentra ningún enlace

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][fuente]

Obtener información acerca de los archivos adicionales en el directorio del libro.

Parámetros:

book_id – el ID del libro en la base de datos

pattern – el patrón de nombres de archivo a buscar. Un patrón vacío coincide con todos los archivos adicionales. Los patrones deben usar / como separador. Use la constante DATA_FILE_PATTERN para buscar archivos dentro del directorio de datos.

Devuelve:

Una tupla con todos los archivos adicionales que coinciden con el patrón especificado. Cada elemento de la tupla es de la forma ExtraFile(ruta_rel, ruta_archivo, res_stat), donde ruta_rel es la ruta relativa al archivo respecto al directorio del libro usando / como separador. res_stat es el resultado de ejecuta os.stat() sobre el archivo.

merge_extra_files(dest_id, src_ids, replace=False)[fuente]

Combinar los archivos adicionales de src_ids (origen) con los de dest_id (destino). A los archivos que entren en conflicto se les cambiará el nombre automáticamente, a no ser que replace sea True, en cuyo caso se sustituirán.

move_book_from_trash(book_id)[fuente]

Restaurar un libro del directorio de la papelera

move_format_from_trash(book_id, fmt)[fuente]

Restaurar un formato del directorio de la papelera

multisort(fields, ids_to_sort=None, virtual_fields=None)[fuente]

Devuelve una lista de identificadores de libros ordenados. Si ids_to_sort es None, devuelve todos los identificadores de libros.

los campos deben ser una lista de 2-tuplas de la forma (field_name, ascending=True|False). El campo más significativo es la primera 2-tupla.

notes_data_for(field, item_id) → str[fuente]

Devuelve todos los datos de notas como un diccionario o None si la nota no existe

notes_for(field, item_id) → str[fuente]

Devuelve el documento de notas o un texto vacío si no se encuentra

notes_resources_used_by(field, item_id)[fuente]

Devuelve el conjunto de códigos de comprobación de recurso de todos los recursos usados por la nota del elemento especificado

pref(name, default=None, namespace=None)[fuente]

Devuelve el valor de la preferencia especificada o el valor especificado como default si la preferencia no se ha establecido.

read_backup(book_id)[fuente]

Devuelve la copia de seguridad de los metadatos OPF para el libro como una serie de bytes o None si no existe dicha copia de seguridad.

remove_books(book_ids, permanent=False)[fuente]

Eliminar los libros especificados por los book_ids de la base de datos y eliminar sus archivos de formato. Si permanent es False, los archivos de formato se dejan en el directorio de papelera específico de la biblioteca.

remove_formats(formats_map, db_only=False)[fuente]

Eliminar los formatos especificados de los libros especificados.

Parámetros:

formats_map – Un mapa de book_id a una lista de formatos para eliminar del libro.

db_only – Si es True, sólo se elimina el registro del formato de la base de datos, no se borra el archivo de formato del sistema de archivos.

Devuelve:

Un mapa de id de libro a conjunto de formatos realmente eliminados del sistema de archivos para ese libro

remove_items(field, item_ids, restrict_to_book_ids=None)[fuente]

Elimina todos los elementos del campo especificado con los id especificados. Devuelve el conjunto de id de libro afectados. restrict_to_book_ids es un conjunto de id de libros opcional. Si se especifica, sólo se eliminarán los elementos de estos libros.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[fuente]

Cambiar el nombre de los archivos de datos adicionales

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[fuente]

Cambiar el nombre de elementos de un campo de tipo varios-uno o varios-varios, como etiquetas o series.

Parámetros:

change_index – Al cambiarel nombre de un campo del tipo serie cambiar también los valores de «series_index».

restrict_to_book_ids – Un conjunto de id de libros opcional sobre los que se realiza el cambio de nombre, de manera predeterminada incluye todos los libros.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[fuente]

Restaurar la entrada del libro en la base de datos de un libro que ya existe en el sistema de archivos.

restore_original_format(book_id, original_fmt)[fuente]

Restaurar el formato especificado a partir del ORIGINAL_FORMAT previamente guardado, si lo hubiera. Devuelve True en caso de éxito. El ORIGINAL_FORMAT se elimina después de una restauración correcta.

property safe_read_lock

Un bloqueo de lectura segura es un bloqueo que no hace nada si el hilo ya tiene un bloqueo de escritura, de lo contrario, agrega un bloqueo de lectura. Esto es necesario para evitar DowngradeLockErrors, lo que puede suceder cuando se actualiza la caché de búsqueda en presencia de columnas compuestas. Al actualizar la caché de búsqueda se crea un bloqueo exclusivo, pero al buscar en una columna compuesta tiene lugar la lectura de valores de campo vía ProxyMetadata, que trata de obtener un bloqueo compartido. Puede haber otras situaciones que desencadenen esto.

Esta propiedad devuelve un nuevo objeto de bloqueo en cada acceso. Este objeto de bloqueo no es recursivo (por rendimiento) y sólo debe ser utilizado en una sentencia with como with cache.safe_read_lock:, de lo contrario habrá problemas.

save_original_format(book_id, fmt)[fuente]

Guardar una copia del formato especificado como ORIGINAL_FORMAT, sustituyendo cualquier ORIGINAL_FORMAT existente.

search(query, restriction='', virtual_fields=None, book_ids=None)[fuente]

Buscar en la base de datos con la consulta especificada, devolviendo un conjunto de identificadores de libros que coincidan.

Parámetros:

restriction – Una restricción que se añade como Y a la consulta especificada. Tenga en cuenta que las restricciones se almacenan en caché, por lo tanto, la búsqueda de a Y b será más lenta que a con una restricción b.

virtual_fields – Usado internamente (campos virtuales tales como «on_device» para búsquedas).

book_ids – Si no es None, un conjunto de identificadores de libros en los que buscar en vez de buscar en todos los libros.

search_notes(fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=<class 'tuple'>, process_each_result=None, limit=None)[fuente]

Busca en el texto de las notas usando un índice de búsqueda de texto completo. Si la consulta está vacía devuelve todas las notas.

set_conversion_options(options, fmt='PIPE')[fuente]

options debe ser un mapa de la forma {book_id:opciones de conversión}

set_cover(book_id_data_map)[fuente]

Establecer la portada de este libro. El argumento puede ser un objeto de tipo QImage, QPixmap, archivo o una serie de bytes. También puede ser None, en cuyo caso se elimina cualquier portada existente.

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[fuente]

Establecer los valores del campo especificado por name. Devuelve el conjunto de todos los identificadores de libros afectados por el cambio.

Parámetros:

book_id_to_val_map – Mapa de book_ids a los valores que se deben aplicar.

allow_case_change – Si es True, se cambiará el uso de mayúsculas y minúsculas de campos de tipo varios-uno o varios-varios. Por ejemplo, si un libro tiene la etiqueta «etiqueta1» y establece la etiqueta de otro libro a «Etiqueta1», entonces los libros de ambos tendrá la etiqueta «Etiqueta1» si allow_case_change es True, de lo contrario ambos tendrán la etiqueta «etiqueta1».

do_path_update – Usado internamente, nunca debe cambiarse.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[fuente]

Asigna enlaces para los elementos en el campo field. Tenga en cuenta que este método no cambia los valores que no estén presentes en value_to_link_map

Parámetros:

field – el nombre de búsqueda

value_to_link_map – dict(valor_campo:enlace, ...). Tenga en cuenta que son valores, no identificadores de campo.

Devuelve:

libros modificados al asignar el enlace

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[fuente]

Aplicar metadatos para el libro id a partir del objeto Metadata mi

Si se establece force_changes=True set_metadata actualizará los campos, incluso si mi contiene valores vacíos. En este caso, None se distingue de «vacío». Si mi.XXX es None, el XXX no se sustituye, en caso contrario sí. Las etiquetas, identificadores y los atributos de portadas son casos especiales. Las etiquetas e identificadores no se pueden establecer en None por lo que siempre se sustituirán si force_changes es True. Debe asegurarse de que mi contenga los valores que desea que tenga el libro. Las portadas siempre se cambian si se proporciona una nueva, pero nunca se borran. Tenga también en cuenta que force_changes no tiene ningún efecto en la configuración de título o autores.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[fuente]

Establece el documento de notas. Si el texto que se puede buscar es distinto del documento, debe especificarse como searchable_text. Si el documento hace referencia a recursos, sus códigos de comprobación deben incluirse en resource_hashes. Establecer remove_unused_resources como True para limpiar los recursos no usados, tenga en cuenta que en todo caso una nota limpia automáticamente los recursos pertenecientes a esa nota.

set_pref(name, val, namespace=None)[fuente]

Establecer la preferencia especificada al el valor especificado. Ver también pref().

split_if_is_multiple_composite(f, val)[fuente]

Si f es una clave de búsqueda de una columna compuesta y la columna es is_multiple, entonces val se divide en valores únicos no vacíos. La comparación distingue mayúsculas y minúsculas. El orden no se mantiene. Devuelve un objeto list() por compatibilidad con otros métodos de obtención de metadatos como las etiquetas.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[fuente]

Devuelve los identificadores de todos los libros que tienen la etiqueta tag que son anteriores al momento especificado. La comparación de etiquetas no distingue entre mayúsculas y minúsculas.

Parámetros:

delta – Un objeto de tipo timedelta o None. Si es None, se devuelven todos los identificadores con etiqueta.

must_have_tag – Si no es None, la lista de coincidencias se limitará a los libros que tengan esta etiqueta

must_have_authors – Una lista de autores. Si no es None, la lista de coincidencias se limitará a los libros que tengan estos autores (no distingue mayúsculas y minúsculas).

unretire_note_for(field, item_id) → int[fuente]

Desretira una nota previamente retirada para el elemento especificado. Las notas se retiran cuando se elimina un elemento de la base de datos

user_categories_for_books(book_ids, proxy_metadata_map=None)[fuente]

Devuelve las categorías de usuario de los libros especificados. proxy_metadata_map es opcional y es útil para aumentar el rendimiento en contextos donde ya existe un objeto ProxyMetadata de los libros. Debe ser un mapa de book_ids a los objetos ProxyMetadata correspondientes.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Configurar un entorno de desarrollo de calibre »

Documentación de la API para las herramientas de modificación de libros electrónicos

Documentación de la API para las herramientas de modificación de libros electrónicos

Las herramientas de modificación de libros electrónicos consisten en un objeto calibre.ebooks.oeb.polish.container.Container que representa un libro como colección de archivos HTML y de recursos, y varias herramientas que pueden usarse para realizar diversas operaciones sobre el contenedor. Todas las herramientas están bajo la forma de funciones a nivel de módulo, en los distintos módulos calibre.ebooks.oeb.polish.*.

Se obtiene un objeto contenedor para un libro en una ruta como esta:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

Si está escribiendo un complemento para el editor de libros electrónicos, puede obtener el contenedor actual para el libro que se está modificando de este modo:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
    report_error # No book has been opened yet

El objeto contenedor

class calibre.ebooks.oeb.polish.container.Container(rootpath, opfpath, log, clone_data=None)[fuente]

Un contenedor representa un libro electrónico abierto como una carpeta con archivos más un archivo opf. Hay varios conceptos importantes:

La carpeta raíz. Ésta es la base del libro electrónico. Todos los archivos del libro se encuentran en esta carpeta o en sus subcarpetas.

Nombres: Son rutas a los archivos del libro con respecto a la carpeta raíz. Siempre contienen separadores POSIX, sin comillas. Pueden considerarse como identificadores canónicos para los archivos del libro. La mayoría de los métodos en el objeto contenedor trabajan con nombres. Los nombres están siempre en la forma normalizada Unicode NFC.

Clones: el objeto contenedor soporta clonación eficiente en disco, que se utiliza para implementar hitos en el editor de libros electrónicos. Para que esto funcione, nunca se debe acceder a los archivos del sistema de archivos directamente. En su lugar, debe usarse raw_data() o open() para leer o escribir en los archivos que integran el libro.

Al convertir entre hrefs y nombres utilice los métodos suministrados por esta clase, éstos asumen que todas las hrefs están entre comillas.

abspath_to_name(fullpath, root=None)[fuente]

Convierte una ruta absoluta en un nombre canónico con respecto a root

Parámetros:

root – La carpeta base. De manera predeterminada se usa la raíz del objeto contenedor.

add_file(name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None)[fuente]

Añade un archivo a este contenedor. Se crean automáticamente las entradas para el archivo en el manifiesto OPF y el lomo (si el archivo es un documento de texto)

add_name_to_manifest(name, process_manifest_item=None)[fuente]

Añade una entrada al manifiesto para un archivo con el nombre especificado. Devuelve el id del manifiesto.

add_properties(name, *properties)[fuente]

Añade las propiedades especificadas (properties) al elemento del manifiesto identificado por name.

apply_unique_properties(name, *properties)[fuente]

Se asegura de que las propiedades especificadas (properties) estás establecidas sólo en el elemento del manifiesto identificado por name. Pueda usarse None como name para eliminar la propiedad de todos los elementos.

book_type = 'oeb'

El tipo de libro (epub para archivos EPUB o azw3 para archivos AZW3)

commit(outpath=None, keep_parsed=False)[fuente]

Almacena todos los objetos procesados modificados en el sistema de archivos y escribe el archivo del libro en outpath.

Parámetros:

output – La ruta de acceso donde se escribe el archivo del libro. Si es None, se usa la ruta del archivo original.

keep_parsed – Si es True, la representación procesada de los elementos almacenados se mantiene en memoria.

commit_item(name, keep_parsed=False)[fuente]

Almacena un objeto procesado en el disco (se serializa y escribe en el archivo subyacente). Si keep_parsed es True la representación procesada se mantiene en el caché. Véase también: parsed()

dirty(name)[fuente]

Marca el objeto analizado correspondiente a nombre como sucio. Véase también parsed().

exists(name)[fuente]

True si y sólo si existe un archivo o carpeta que corresponde con el nombre canónico name. Tenga en cuenta que esta función sufre las limitaciones del sistema de archivos del sistema operativo subyacente, en particular la distinción o no entre mayúsculas y minúsculas. En un sistema de archivos que no distingue entre mayúsculas y minúsculas, esta función devuelve True incluso si name y el archivo en el sistema de archivos difieren en mayúsculas y minúsculas. Véase también has_name().

filesize(name)[fuente]

Devuelve el tamaño en bytes del archivo representado por el nombre canónico name. Gestiona automáticamente objectos procesados modificados. Véase también parsed().

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[fuente]

Añade un elemento al manifiesto con href derivado del name dado. Asegura la unicidad del href y del id automáticamente. Devuelve el elemento generado.

get_file_path_for_processing(name, allow_modification=True)[fuente]

Parecido a open() pero devuelve una ruta de archivo en lugar de un objeto de archivo abierto.

property guide_type_map

Mapa de tipo de guía a nombre canónico

has_name(name)[fuente]

Devuelve True si y sólo si existe un archivo con el mismo nombre canónico que el name especificado. A diferencia de exists(), este método siempre distingue mayúsculas y minúsculas.

href_to_name(href, base=None)[fuente]

Convierte un href (relativo a base) a un nombre. base debe ser un nombre o None, en cuyo caso se usa self.root.

insert_into_xml(parent, item, index=None)[fuente]

Inserta item en parent (o lo añade al final si index es None), arreglando la sangría. Sólo funciona con elementos con cierre incorporado.

is_dir = False

Si este contenedor representa un libro descomprimido (un directorio)

iterlinks(name, get_line_numbers=True)[fuente]

Itera sobre todos los enlaces en name. Si get_line_numbers es True, los resultados producidos son de la forma (enlace, número_de_línea, desplazamiento). Donde «número_de_línea» es número de línea en el que aparece el enlace y «desplazamiente» es el número de caracteres desde el inicio de la línea. Tenga en cuenta que el desplazamiento puede de hecho abarcar varias líneas si no es cero.

make_name_unique(name)[fuente]

Se asegura de que name no existe ya en este libro. Si existe, devuelve una versión modificada que no exista.

manifest_has_name(name)[fuente]

Devuelve True si el manifiesto tiene una entrada correspondiente a name

property manifest_id_map

Correspondencia entre id del manifiesto y nombres canónicos

manifest_items_of_type(predicate)[fuente]

Los nombres de todos los elementos del manifiesto cuyo media-type coincide con predicate. predicate puede ser un conjunto, una lista, un texto o una función que toma un único argumento, que se ejecutará con el media-type.

manifest_items_with_property(property_name)[fuente]

Todos los elementos del manifiesto que tienen la propiedad especificada

property manifest_type_map

Correspondencia entre media-type del manifiesto y lista de nombres canónicos de cada media-type

property mi

Los metadatos de este libro como un objeto Metadata. Tenga en cuenta que este objeto se construye al vuelo cada vez que se solicita la propiedad, por lo que debe usarse con moderación.

name_to_abspath(name)[fuente]

Convierte un nombre canónico en una ruta de acceso absoluta acorde al sistema operativo

name_to_href(name, base=None)[fuente]

Convierte un nombre a un href relativo a la base, que debe ser un nombre o None, en cuyo caso self.root se toma como la base

property names_that_must_not_be_changed

Conjunto de nombres que nunca deben cambiarse. Depende del formato de archivo del libro electrónico.

property names_that_must_not_be_removed

Conjunto de nombres que nunca deben borrarse del contenedor. Depende del formato de archivo del libro electrónico.

property names_that_need_not_be_manifested

Conjunto de nombres que pueden estar ausentes del manifiesto. Depende del formato de archivo del libro electrónico.

open(name, mode='rb')[fuente]

Abre el archivo apuntado por name para lectura o escritura directa. Tenga en cuenta que esto almacenará el archivo si se modifica y lo eliminará del caché de procesamiento. Debe acabar con este archivo antes de acceder de nuevo a la versión procesada u ocurrirán cosas malas.

property opf

El archivo OPF analizado

opf_get_or_create(name)[fuente]

Método práctico que devuelve el primer elemento XML con el nombre especificado (name) o lo crea bajo el elemento opf:package y luego lo devuelve, si no existe previamente.

property opf_version

La versión establecida en el elemento <package> del OPF

property opf_version_parsed

La versión establecida en el elemento <package> del OPF, como una tupla de enteros

opf_xpath(expr)[fuente]

Método práctico para evaluar una expresión XPath en el archivo OPF, tiene los prefijos de espacio de nombres «opf:» y «dc:» predefinidos.

parsed(name)[fuente]

Devuelve una representación procesada del archivo especificado por name. Para archivos HTML y XML, devuelve un árbol lxml. Para archivos CSS devuelve una hoja de estilos css_parser. Tenga en cuenta que los objetos procesados se mantienen en caché para mejorar el rendimiento. Si hace algún cambio en el objeto procesado, debe ejecutar dirty() para que el contenedor sepa que tiene que actualizar el caché. Véase también replace().

raw_data(name, decode=True, normalize_to_nfc=True)[fuente]

Devuelve el contenido en bruto correspondiente al archivo especificado por su name

Parámetros:

decode – Si es True y el archivo tiene un tipo MIME basado en texto, lo descodifica y devuelve un objeto unicode en lugar de bytes en bruto.

normalize_to_nfc – Si es True el objeto Unicode devuelto se normaliza a la norma NFC, como requieren los formatos de archivo EPUB y AZW3.

relpath(path, base=None)[fuente]

Convierte una ruta absoluta (con separadores de sistema operativo) a una ruta relativa a base (de manera predeterminada, self.root). La ruta relativa no es un nombre. Use abspath_to_name() para obtener un nombre.

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[fuente]

Elimina los elementos especificados (por nombre canónico) del lomo. Si remove_if_no_longer_in_spine es True, los elementos también se borran del libro, no sólo del lomo.

remove_from_xml(item)[fuente]

Elimina item del elemento superior, arreglando la sangría (sólo funciona con elementos con cierre incorporado)

remove_item(name, remove_from_guide=True)[fuente]

Elimina el elemento identificado por name de este contenedor. Esto elimina todas las referencias al elemento del manifiesto, guía y lomo del OPF, así como de cualquier caché interno.

rename(current_name, new_name)[fuente]

Cambia el nombre de un archivo de current_name a new_name. Cambia automáticamente la base de todos los enlaces en el archivo si cambia la carpeta en que se encuentra el archivo. Tenga en cuenta, no obstante, que no se actualizan los enlaces en otros archivos que puedan hacer referencia a éste. Esto es así por motivos de rendimiento, tales actualizaciones deben hacerse una sola vez en masa.

replace(name, obj)[fuente]

Sustituye el objeto procesado correspondiente a name por obj, que debe ser un objeto similar, es decir, un árbol lxml para archivos HTML o XML o una hoja de estilos css_parser para archivos CSS.

replace_links(name, replace_func)[fuente]

Sustituye todos los enlaces en name usando replace_func, que debe ser un objeto ejecutable que acepte un URL y devuelva el URL sustituido. También debe tener un atributo replaced que sea True si se ha efectuado alguna sustitución. Pueden crearse tales objetos ejecutables de forma práctica usando las clases LinkReplacer y LinkRebaser.

serialize_item(name)[fuente]

Convierte un objeto analizado (identificado por su nombre canónico) en una cadena de bytes. Vea parsed().

set_spine(spine_items)[fuente]

Establece el lomo como spine_items, donde spine_items es un objeto iterable de la forma (nombre, lineal). Se producirá un error si alguno de los nombres no está presente en el manifiesto.

property spine_items

Un iterador que proporciona la ruta de acceso para cada elemento del lomo de los libros. Véase también: spine_iter y spine_items.

property spine_iter

Un iterador que proporciona elemento, nombre, es_lineal para cada elemento del lomo de los libros. elemento es el elemento lxml, nombre es el nombre canónico del archivo y es_lineal es True si el elemento es lineal. Véase también: spine_names y spine_items.

property spine_names

Un iterador que proporciona nombre y es_lineal para cada elemento del lomo de los libros. Véase también: spine_iter y spine_items.

Gestionar los archivos dentro de un contenedor

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[fuente]

Sustituye enlaces a archivos en el contenedor. Iterará sobre todos los archivos en el contenedor y cambiará los enlaces especificados en ellos.

Parámetros:

link_map – Un mapa de nombre canónico antiguo a nombre canónico nuevo. Por ejemplo: {'images/antiguo.png': 'images/nuevo.png'}

frag_map – Un objeto ejecutable que toma dos argumentos (name, anchor) y devuelve un punto de anclaje. Esto es útil si tiene que cambiar los puntos de anclaje en archivos HTML. De manera predeterminada, no hace nada.

replace_in_opf – Si es False, no se sustituyen los enlaces en el archivo OPF.

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[fuente]

Cambia el nombre de archivos en el contenedor, actualizando automáticamente actualizando todos los enlaces a que apunten a ellos.

Parámetros:

file_map – Un mapo de nombres canónicos antiguo a nombre canónico nuevo, por ejemplo: {'text/capitulo1.html': 'capitulo1.html'}.

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[fuente]

Devuelve las carpetas recomendadas para los nombres de archivo dados. La recomendación se basa en dónde se encuentra la mayoría de los archivos del mismo tipo en el contenedor. Si no hay archivos de un tipo concreto, la carpeta recomendada es la que contiene el archivo OPF.

Presentación mejorada y corrección automática de errores

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[fuente]

Corrige cualquier error de procesado en el HTML representado como un texto en raw. La corrección se realiza usando el algoritmo de procesado HTML5.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[fuente]

Corrige cualquer error de procesado en todos los archivos HTML del contenedor. La corrección ser realiza usando el algoritmo de procesado HTML5.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[fuente]

Redistribuye el HTML representado como un texto en raw

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[fuente]

Redistribuye el CSS representado como un texto en raw

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[fuente]

Redistribuye el XML representado como un texto en raw. Si name es el nombre del OPF, se realiza una redistribución adicional específica para OPF.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[fuente]

Redistribuye todos los archivos HTML, CSS y XML del contenedor

Gestionar las sobrecubiertas de los libros

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[fuente]

Elimina una sobrecubierta existente, si la hay. Devuelve False si no se encontró una sobrecubierta.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[fuente]

Crea una nueva sobrecubierta a partir de los metadatos del libro o sustituye una sobrecubierta existente. Devuelve True si se sustutuyó una sobrecubierta existente.

Dividir y combinar archivos

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[fuente]

Divide el archivo especificado por name en la ubicación especificada por loc_or_xpath. La división migra automáticamente todos los enlaces y referencias a los archivos afectados.

Parámetros:

loc_or_xpath – Debería ser una expresión XPath como «//h:div[@id=»dividir_aqui»]». También puede ser un loc, que se usa internamente para ejecutar la división en el panel de vista previa.

before – Si es True, la división ocurre antes del elemento identificado, en caso contrario ocurre después.

totals – Usado internamente

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[fuente]

Divide el archivo especificado en múltiples ubicaciones (todas las etiquetas que coincidan con la expresión XPath especificada). Véase también split(). La división migra automáticamente todos los enlaces y referencias a los archivos afectados.

Parámetros:

before – Si es True las divisiones se producen antes del elemento identificado, en caso contrario se producen después.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[fuente]

Combina los archivos especificados en un único archivo, migrando automáticamente todos los enlaces y referencias a los archivos afectados. Los archivos deben ser todos HTML o todos CSS.

Parámetros:

category – Debe ser 'text' para archivos HTML o 'styles' para archivos CSS

names – La lista de archivos para combinar.

master – Cuál de los archivos combinados es el archivo maestro, es decir, el archivo que se mantendrá después del proceso.

Gestionar portadas

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[fuente]

Establece la imagen a la que apunta cover_path como la portada del libro.

Parámetros:

cover_path – La ruta absoluta a un archivo de imagen o el nombre canónico de una imagen en el libro. Si usa una imagen del libro, debe especificar también options, ver más abajo.

report – Un objeto ejecutable opcional que toma un solo argumento. Se ejecutará con información sobre las tareas que se llevan a cabo.

options – None o un diccionario que controla cómo se establece la portada. El diccionario puede tener entradas: keep_aspect: True o False (mantiene la proporción de las portadas en EPUB) no_svg: True o False (Usa un envoltorio de portada SVG en la página de título EPUB) existing: True o False (cover_path se refiere a una imagen existente en el libro)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[fuente]

Marca la imagen especificada como la imagen de portada.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[fuente]

Marca el archivo HTML especificado como la página de título del EPUB.

Parámetros:

move_to_start – Si es True, el archivo HTML se mueve al inicio del lomo

Trabajar con CSS

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[fuente]

Cambia un tipo de letra de old_name a new_name. Modifica todas las ocasiones en que aparece el tipo de letra en hojas de estilo, etiquetas estilo y atributos de estilo. Si old_name se refiere a un tipo de letr incrustado, se elimina. Puede establecer new_name como None para eliminar el tipo de letra en lugar de cambiarlo.

calibre.ebooks.oeb.polish.css.remove_unused_css(container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False)[fuente]

Elimina todas las reglas CSS no utilizadas en el libro. Una regla CSS sin usar es una que no coincide con ningún contenido real.

Parámetros:

report – Un objeto ejecutable opcional que toma un único argumento. Se ejecuta con información sobre las operaciones que se lleva a cabo.

remove_unused_classes – Si es True, también se eliminan los atributos de clase del HTML que no correspondan a ninguna regla CSS.

merge_rules – Si es True, se combinan las reglas con selectores idénticos.

merge_rules_with_identical_properties – Si es True, se combinan las reglas con propiedades idénticas.

remove_unreferenced_sheets – Si es True, se eliminan las hojas de estilos que no tienen referencias en otros archivos

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[fuente]

Elimina las propiedades CSS especificadas de todas las reglas CSS del libro.

Parámetros:

properties – Conjunto de propiedades para eliminar. Por ejemplo: {'font-family', 'color'}.

names – Los archivos en los que se eliminan las propiedades. De manera predeterminada, todos los archivos HTML y CSS del libro.

Trabajar con el índice

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[fuente]

Genera un índice a partir de una lista de expresiones XPath. Cada expresión de la lista corresponde a un nivel del índice generado. Por ejemplo: ['//h:h1', '//h:h2', '//h:h3'] generará un índice de tres niveles a partir de las etiquetas <h1>, <h2> y <h3>.

calibre.ebooks.oeb.polish.toc.from_links(container)[fuente]

Genera un índice a partir de los enlaces del libro.

calibre.ebooks.oeb.polish.toc.from_files(container)[fuente]

Genera un índice a partir de los archivos del libro.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[fuente]

Crea un índice explícito (HTML) a partir de un índice NCX existente.

Parámetros:

title – El título de este índice.

Herramienta para modificar libro

class calibre.gui2.tweak_book.plugin.Tool[fuente]

Bases: object

La clase básica para las herramientas individuales en un complemento para modificar libros. Algunos miembros útiles incluyen:

self.plugin: Una referencia al objeto calibre.customize.Plugin al que pertenece esta herramienta.

self. boss

self. gui

Métodos que deben reemplazarse en subclases:

create_action()

register_shortcut()

name = None

Especifique un nombre único que se utilizará como clave

allowed_in_toolbar = True

Si es True, el usuario puede colocar esta herramienta en la barra de herramientas de complementos

allowed_in_menu = True

Si es True, el usuario puede colocar esta herramienta en el menú de complementos

toolbar_button_popup_mode = 'delayed'

El modo emergente para el menú (si lo hay) del botón de la barra de herramientas. Los valores posibles son «delayed», «instant», «button»

property boss

El objeto calibre.gui2.tweak_book.boss.Boss. Utilizado para controlar la interfaz de usuario.

property gui

La ventana principal de la interfaz del usuario

property current_container

Devuelve el objeto calibre.ebooks.oeb.polish.container.Container actual que representa el libro que se está modificando.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[fuente]

Registra un atajo de teclado que ejecutará la qaction especificada. Este atajo de teclado será automáticamente personalizable por el usuario a través de la sección Atajos de teclado de las preferencias del editor.

Parámetros:

qaction – Un objeto QAction, se ejecutará cuando el usuario pulse la combinación de teclas configurada.

unique_name – Un nombre único para este atajo o acción. Se usará internamente, no debe coincidir con ninguna otra acción de este complemento.

default_keys – Una lista de los atajos de teclado predeterminados. Si no se especifica, no se establecerá ningún atajo predeterminado. Si los atajos especificados están en conflicto on atajos predefinidos, con la configuración del usuario o con otros complementos, no se tendrán en cuenta. En tal caso, los usuarios tendrán que configurar los atajos manualmente a través de las Preferencias. Por ejemplo: default_keys=('Ctrl+J', 'F9').

short_text – Una breve descripción opcional de esta acción. Si no se especifica, se utilizará el texto de la QAction.

description – Una descripción opcional más extensa de esta acción, que se usará en la entrada de las preferencias para este acceso directo.

create_action(for_toolbar=True)[fuente]

Crea una QAction que se añadirá a la barra de herramientas de complementos, o al menú de complementos según el valor de for_toolbar. Por ejemplo:

def create_action(self, for_toolbar=True):
    ac = QAction(get_icons('myicon.png'), 'Do something')
    if for_toolbar:
        # We want the toolbar button to have a popup menu
        menu = QMenu()
        ac.setMenu(menu)
        menu.addAction('Do something else')
        subaction = menu.addAction('And another')

        # Register a keyboard shortcut for this toolbar action be
        # careful to do this for only one of the toolbar action or
        # the menu action, not both.
        self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',))
    return ac

Ver también

Método register_shortcut().

Controlar la interfaz de usuario del editor

La interfaz de usuario del editor de libros electrónicos está controlada por un único objeto global Boss. Este objeto posee varios métodos útiles que pueden utilizarse en el código de los complementos para realizar diversas tareas.

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[fuente]

add_savepoint(msg)[fuente]

Crea un hito de restauración con el nombre especificado como msg

apply_container_update_to_gui(mark_as_modified=True)[fuente]

Actualiza todos los componentes de la interfaz para reflejar los datos más recientes del contenedor de libro actual.

Parámetros:

mark_as_modified – Si es True, el libro se marcará como modificado, por lo que al usuario se le pedirá guardarlo al salir.

close_editor(name)[fuente]

Cierra el editor que está modificando el archivo especificado por name

commit_all_editors_to_container()[fuente]

Almacena todos los cambios que el usuario haya hecho en los archivos abiertos en el contenedor. Debe ejecutar este método antes de realizar cualquier acción sobre el contenedor actual.

property currently_editing

Devuelve el nombre del archivo que está se está modificando actualmente o None si no se está modificando ningún archivo.

edit_file(name, syntax=None, use_template=None)[fuente]

Abre el archivo especificado por name en un editor

Parámetros:

syntax – El tipo de medio del archivo, por ejemplo: 'text/html'. Si no se especifica, se supone uno a partir de la extensión del archivo.

use_template – Una plantilla con la que inicializar el editor abierto

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[fuente]

Abre el libro electrónico en path para modificarlo. Mostrará un error si el libro electrónico no está en un formato compatible o si el libro actual tiene cambios sin guardar.

Parámetros:

edit_file – El nombre de un archivo dentro del libro abierto para iniciar la modificación. También puede ser una lista de nombres.

rewind_savepoint()[fuente]

Deshace la creación anterior de un hito de restauración, útil si crea un hito y luego cancela la operación sin ningún cambio

save_book()[fuente]

Guarda el libro. El guardado se lleva a cabo en segundo plano

set_modified()[fuente]

Marca el libro como modificado

show_current_diff(allow_revert=True, to_container=None)[fuente]

Muestra los cambios en el libro desde el estado del último hito

Parámetros:

allow_revert – Si es True el cuadro de diferencias tendrá un botón que le permitirá al usuario deshacer los cambios

to_container – Un objeto de contenedor para compararlo con el contenedor actual. Si es None, se usará el contenedor del hito anterior.

show_editor(name)[fuente]

Muestra el editor que está modificando el archivo especificado por name

sync_preview_to_editor()[fuente]

Sincroniza la posición del panel de previsualización con la posición actual del cursor en el editor.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

siguiente |

anterior |

Inicio »

Gestión de derechos digitales (DRM)

Gestión de derechos digitales (DRM)

Gestión de derechos digitales (digital rights management, DRM) es un término genérico para las tecnologías de control de acceso que pueden ser utilizadas por fabricantes de hardware, editoriales, titulares de derechos de autor e individuos para tratar de imponer limitaciones en el uso de contenido y dispositivos digitales. También, a veces, se describe despectivamente como gestión de restricciones digitales (digital restrictions management). El término se usa para describir cualquier tecnología que inhiba los usos (legítimos o no) de contenido digital no deseados o previstos por el proveedor del contenido. El término generalmente no se refiere a otras formas de protección de copia que se pueden eludir sin modificar el archivo o dispositivo, como números de serie o archivos de clave. También puede referirse a restricciones asociadas con ejemplares específicos de obras o dispositivos digitales. Las tecnologías DRM intentan controlar el uso de medios digitales impidiendo el acceso, copia o conversión a otros formatos por parte de los usuarios finales. Ver Wikipedia [https://es.wikipedia.org/wiki/Gesti%C3%B3n_de_derechos_digitales].

¿Qué implica DRM para mí personalmente?

Cuando usted compra un libro electrónico con DRM no lo posee realmente, sino que ha comprado el permiso para usarlo de la manera indicada por el vendedor. El DRM limita lo que puede hacer con los libros electrónicos que ha «comprado». A menudo las personas que compran libros con DRM no están al tanto del alcance de estas restricciones. Estas restricciones le impiden cambiar el formato del libro a su gusto, incluyendo cambios en de estilo como ajustar el tamaño de letra, aunque hay programas que le permiten hacerlo para libros sin DRM. La gente suele sorprenderse de que un libro que han comprado en un formato concreto no pueda convertirse a otro si el libro tiene DRM. Así, si tiene un Kindle de Amazon y compra un libro vendido por Barnes & Noble, debería saber que si ese libro tiene DRM no va a poder leerlo en el Kindle. Tenga en cuenta que estoy hablando de un libro que ha comprado, no robado o pirateado, sino COMPRADO.

¿Qué hace DRM por los autores?

Los editores de libros electrónicos con DRM aducen que el DRM es por el bien de los autores, para proteger su integridad artística y evitar la piratería. Pero el DRM NO evita la piratería. Las personas que quieren piratear contenido o usar contenido pirateado siguen haciéndolo con éxito. Los tres principales de esquemas de DRM para libros electrónicos a día de hoy son los usados por Amazon, Adobe y Barnes & Noble, y los tres han sido descifrados. Todo lo que hace el DRM es incomodar a los usuarios legítimos. Se puede argumentar que en realidad perjudica a los autores, ya que las personas que comprarían el libro eligen en su lugar buscar una versión pirateada, ya que no están dispuestos a soportar el DRM. Aquellos que piratearían en ausencia de DRM lo hacen también en su presencia. Insisto, el punto clave es que el DRM no evita la piratería. Por lo tanto, el DRM no solo es inútil y perjudicial para los compradores de libros electrónicos, sino también una pérdida de dinero.

DRM y libertad

Aunque el contenido digital se puede utilizar para hacer que la información, así como las obras creativas, estén fácilmente disponibles para todos y empoderar a la humanidad, esto no interesa a algunos editores que desean alejar a las personas de esta posibilidad de libertad simplemente para mantener su relevancia en un mundo que se desarrolla tan rápidamente que no pueden seguir el ritmo.

¿Por que calibre no admite DRM?

calibre es software de código abierto, mientras que DRM por su propia naturaleza es cerrado. Si calibre permitiera abrir o ver archivos DRM, podría modificarse trivialmente para usarlo como una herramienta para la eliminación de DRM, lo cual es ilegal según las leyes actuales. El software de código abierto y el DRM representan un choque de principios. Mientras que el DRM trata de controlar al usuario, el software de código abierto trata de dar más poder al usuario. Simplemente, los dos no pueden coexistir.

¿Cual es la posición de calibre respecto a los proveedores de contenido?

Creemos firmemente que los autores y otros proveedores de contenido deben ser compensados por sus esfuerzos, pero el DRM no es la forma de hacerlo. Estamos desarrollando esta base de datos de libros electrónicos sin DRM de diversas fuentes para ayudarle a encontrar alternativas sin DRM y para ayudar a autores y editores independientes de libros electrónicos sin DRM a publicar su contenido. Esperamos que la encuentre útil y le pedimos que no piratee el contenido disponible aquí.

¿Como puedo ayudar a combatir el DRM?

Como alguien que lee y compra libros electrónicos, puede ayudar a combatir el DRM. No compre libros electrónicos con DRM. Hay algunos editores que publican libros electrónicos sin DRM. Haga un esfuerzo para ver si ofrecen el libro electrónico que está buscando. Si le gustan los libros de ciertos autores independientes que venden libros electrónicos sin DRM y puede permitírselo, haga donaciones para ellos. Éste es dinero bien gastado, ya que sus libros electrónicos suelen ser más baratos (puede haber excepciones) que los que compraría a los editores de libros con DRM y probablemente funcionará en todos los dispositivos que tenga en el futuro, ahorrándole el costo de comprar el libro nuevamente. No desanime a los editores y autores de libros electrónicos sin DRM pirateando su contenido. Los proveedores de contenido merecen una compensación por sus esfuerzos. No los castigue por intentar mejorar su experiencia de lectura poniendo a disposición libros electrónicos sin DRM. A la larga, esto es perjudicial para usted. Si ha comprado libros de vendedores que ofrecen tanto libros con DRM como sin DRM, sin saber si llevan DRM o no, deje un comentario o una revisión en el sitio web informando a los futuros compradores sobre la presencia de DRM. Muchos vendedores no creen que sea importante indicar claramente a sus compradores si un libro electrónico contiene DRM o no. Aquí <https://www.defectivebydesign.org/guide/ebooks> encontrará una guía para vivir sin DRM (en inglés).

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

anterior |

Inicio »

Glosario

Glosario

RSS

RSS (Really Simple Syndication) es un formato agregador de contenido de Internet que se utiliza para publicar contenido actualizado con frecuencia, como artículos de noticias, blogs, etc. Se trata de un formato que es especialmente adecuado para ser leído por una máquina y, por lo tanto, es la forma ideal de obtener contenido de Internet en un libro electrónico. Hay muchos otros formatos de origen en uso en el Internet y calibre entiende la mayoría de ellos. En particular, tiene un buen soporte para el formato ATOM, que se utiliza comúnmente en blogs.

fórmula

Una fórmula es un conjunto de instrucciones que le dicen a calibre cómo convertir una fuente de noticias en línea, como una revista o un blog, en un libro electrónico. Una fórmula es esencialmente código Python [https://www.python.org]. Como tal, es capaz de convertir fuentes de noticias arbitrariamente complejas en libros electrónicos. En el nivel más simple, es sólo un conjunto de variables, como el URL, que le dan a calibre suficiente información para acudir a Internet y descargar la noticia.

HTML

HTML (Hyper Text Mark-Up Language) es un subconjunto del Lenguaje de Marcado Generalizado Estándar (SGML) para la publicación electrónica, es el estándar usado para la World Wide Web.

CSS

CSS (Cascading Style Sheets) es un lenguaje usado para describir cómo debe mostrarse un documento HTML (estilo visual).

API

API (Application Programming Interface) es la interfaz de código fuente que proporciona una biblioteca para recibir las peticiones que se le hagan por parte de programas informáticos.

LRF

LRF El formato de libro electrónico usado por los lectores de libros electrónicos SONY.

URL

URL (Uniform Resource Locator) por ejemplo: http://ejemplo.com

regexp

Las expresiones regulares proporcionan un medio conciso y flexible para identificar fragmentos de texto de interés, como caracteres particulares, palabras o patrones de caracteres. Para una introducción a las expresiones regularel, vea el cursillo.

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Índice de Módulos Python

Índice de Módulos Python

c

| c |
- | calibre |
|     calibre.customize | Defines various abstract base classes that can be subclassed to create plugins.
|     calibre.customize.conversion |
|     calibre.db.cache | The API accessing and manipulating a calibre library.
|     calibre.devices.interface |
|     calibre.ebooks.metadata.book.base |
|     calibre.ebooks.metadata.sources.base |
|     calibre.ebooks.oeb.polish.container | The container object used to represent a book as a collection of its constituent HTML files.
|     calibre.ebooks.oeb.polish.cover |
|     calibre.ebooks.oeb.polish.css |
|     calibre.ebooks.oeb.polish.jacket |
|     calibre.ebooks.oeb.polish.pretty |
|     calibre.ebooks.oeb.polish.replace |
|     calibre.ebooks.oeb.polish.split |
|     calibre.ebooks.oeb.polish.toc |
|     calibre.gui2.tweak_book.boss |
|     calibre.utils.formatter_functions |
|     calibre.web.feeds.news | The API for writing recipes is defined by the :class:`BasicNewsRecipe`

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Resumen: código de modulo

Todos los módulos para los cuales disponen código

PyQt6.QtCore

PyQt6.QtWidgets

calibre.customize

calibre.customize.conversion

calibre.db.cache

calibre.devices.interface

calibre.devices.usbms.cli

calibre.devices.usbms.device

calibre.devices.usbms.driver

calibre.ebooks.metadata.book.base

calibre.ebooks.metadata.sources.base

calibre.ebooks.oeb.polish.container

calibre.ebooks.oeb.polish.cover

calibre.ebooks.oeb.polish.css

calibre.ebooks.oeb.polish.fonts

calibre.ebooks.oeb.polish.jacket

calibre.ebooks.oeb.polish.pretty

calibre.ebooks.oeb.polish.replace

calibre.ebooks.oeb.polish.split

calibre.ebooks.oeb.polish.toc

calibre.gui2.actions

calibre.gui2.preferences

calibre.gui2.tweak_book.boss

calibre.gui2.tweak_book.plugin

calibre.utils.formatter_functions

calibre.web.feeds.news

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.customize

Código fuente para calibre.customize

__license__   = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'

import enum
import importlib
import os
import sys
import zipfile

from calibre.constants import ismacos, iswindows, numeric_version
from calibre.ptempfile import PersistentTemporaryFile

if iswindows:
    platform = 'windows'
elif ismacos:
    platform = 'osx'
else:
    platform = 'linux'

class PluginNotFound(ValueError):
    pass

class InvalidPlugin(ValueError):
    pass

class PluginInstallationType(enum.IntEnum):
    EXTERNAL = 1
    SYSTEM = 2
    BUILTIN = 3

[documentos] class Plugin: # {{{ ''' A calibre plugin. Useful members include: * ``self.installation_type``: Stores how the plugin was installed. * ``self.plugin_path``: Stores path to the ZIP file that contains this plugin or None if it is a builtin plugin * ``self.site_customization``: Stores a customization string entered by the user. Methods that should be overridden in sub classes: * :meth:`initialize` * :meth:`customization_help` Useful methods: * :meth:`temporary_file` * :meth:`__enter__` * :meth:`load_resources` ''' #: List of platforms this plugin works on. #: For example: ``['windows', 'osx', 'linux']`` supported_platforms = [] #: The name of this plugin. You must set it something other #: than Trivial Plugin for it to work. name = 'Trivial Plugin' #: The version of this plugin as a 3-tuple (major, minor, revision) version = (1, 0, 0) #: A short string describing what this plugin does description = _('Does absolutely nothing') #: The author of this plugin author = _('Unknown') #: When more than one plugin exists for a filetype, #: the plugins are run in order of decreasing priority. #: Plugins with higher priority will be run first. #: The highest possible priority is ``sys.maxsize``. #: Default priority is 1. priority = 1 #: The earliest version of calibre this plugin requires minimum_calibre_version = (0, 4, 118) #: The way this plugin is installed installation_type = None #: If False, the user will not be able to disable this plugin. Use with #: care. can_be_disabled = True #: The type of this plugin. Used for categorizing plugins in the #: GUI type = _('Base') def __init__(self, plugin_path): self.plugin_path = plugin_path self.site_customization = None

[documentos] def initialize(self): ''' Called once when calibre plugins are initialized. Plugins are re-initialized every time a new plugin is added. Also note that if the plugin is run in a worker process, such as for adding books, then the plugin will be initialized for every new worker process. Perform any plugin specific initialization here, such as extracting resources from the plugin ZIP file. The path to the ZIP file is available as ``self.plugin_path``. Note that ``self.site_customization`` is **not** available at this point. ''' pass

[documentos] def config_widget(self): ''' Implement this method and :meth:`save_settings` in your plugin to use a custom configuration dialog, rather then relying on the simple string based default customization. This method, if implemented, must return a QWidget. The widget can have an optional method validate() that takes no arguments and is called immediately after the user clicks OK. Changes are applied if and only if the method returns True. If for some reason you cannot perform the configuration at this time, return a tuple of two strings (message, details), these will be displayed as a warning dialog to the user and the process will be aborted. ''' raise NotImplementedError()

[documentos] def save_settings(self, config_widget): ''' Save the settings specified by the user with config_widget. :param config_widget: The widget returned by :meth:`config_widget`. ''' raise NotImplementedError()

[documentos] def do_user_config(self, parent=None): ''' This method shows a configuration dialog for this plugin. It returns True if the user clicks OK, False otherwise. The changes are automatically applied. ''' from qt.core import QApplication, QDialog, QDialogButtonBox, QLabel, QLineEdit, QScrollArea, QSize, Qt, QVBoxLayout from calibre.gui2 import gprefs prefname = 'plugin config dialog:'+self.type + ':' + self.name config_dialog = QDialog(parent) button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) v = QVBoxLayout(config_dialog) button_box.accepted.connect(config_dialog.accept) button_box.rejected.connect(config_dialog.reject) config_dialog.setWindowTitle(_('Customize') + ' ' + self.name) try: config_widget = self.config_widget() except NotImplementedError: config_widget = None if isinstance(config_widget, tuple): from calibre.gui2 import warning_dialog warning_dialog(parent, _('Cannot configure'), config_widget[0], det_msg=config_widget[1], show=True) return False if config_widget is not None: class SA(QScrollArea): def sizeHint(self): sz = self.widget().sizeHint() fw = 2 * self.frameWidth() return QSize(sz.width() + self.verticalScrollBar().sizeHint().width() + fw, sz.height() + fw) sa = SA(config_dialog) sa.setWidget(config_widget) sa.setWidgetResizable(True) v.addWidget(sa) v.addWidget(button_box) if not config_dialog.restore_geometry(gprefs, prefname): QApplication.instance().ensure_window_on_screen(config_dialog) config_dialog.exec() if config_dialog.result() == QDialog.DialogCode.Accepted: if hasattr(config_widget, 'validate'): if config_widget.validate(): self.save_settings(config_widget) else: self.save_settings(config_widget) else: from calibre.customize.ui import customize_plugin, plugin_customization help_text = self.customization_help(gui=True) help_text = QLabel(help_text, config_dialog) help_text.setWordWrap(True) help_text.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard) help_text.setOpenExternalLinks(True) v.addWidget(help_text) sc = plugin_customization(self) if not sc: sc = '' sc = sc.strip() sc = QLineEdit(sc, config_dialog) v.addWidget(sc) v.addWidget(button_box) config_dialog.restore_geometry(gprefs, prefname) config_dialog.exec() if config_dialog.result() == QDialog.DialogCode.Accepted: sc = str(sc.text()).strip() customize_plugin(self, sc) config_dialog.save_geometry(gprefs, prefname) return config_dialog.result()

[documentos] def load_resources(self, names): ''' If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file. For example to load an image:: pixmap = QPixmap() pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png']) icon = QIcon(pixmap) :param names: List of paths to resources in the ZIP file using / as separator :return: A dictionary of the form ``{name: file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary. ''' if self.plugin_path is None: raise ValueError('This plugin was not loaded from a ZIP file') ans = {} with zipfile.ZipFile(self.plugin_path, 'r') as zf: for candidate in zf.namelist(): if candidate in names: ans[candidate] = zf.read(candidate) return ans

[documentos] def customization_help(self, gui=False): ''' Return a string giving help on how to customize this plugin. By default raise a :class:`NotImplementedError`, which indicates that the plugin does not require customization. If you re-implement this method in your subclass, the user will be asked to enter a string as customization for this plugin. The customization string will be available as ``self.site_customization``. Site customization could be anything, for example, the path to a needed binary on the user's computer. :param gui: If True return HTML help, otherwise return plain text help. ''' raise NotImplementedError()

[documentos] def temporary_file(self, suffix): ''' Return a file-like object that is a temporary file on the file system. This file will remain available even after being closed and will only be removed on interpreter shutdown. Use the ``name`` member of the returned object to access the full path to the created temporary file. :param suffix: The suffix that the temporary file will have. ''' return PersistentTemporaryFile(suffix)

def is_customizable(self): try: self.customization_help() return True except NotImplementedError: return False def __enter__(self, *args): ''' Add this plugin to the python path so that it's contents become directly importable. Useful when bundling large python libraries into the plugin. Use it like this:: with plugin: import something ''' if self.plugin_path is not None: from importlib.machinery import EXTENSION_SUFFIXES from calibre.utils.zipfile import ZipFile with ZipFile(self.plugin_path) as zf: extensions = {x.lower() for x in EXTENSION_SUFFIXES} zip_safe = True for name in zf.namelist(): for q in extensions: if name.endswith(q): zip_safe = False break if not zip_safe: break if zip_safe: sys.path.append(self.plugin_path) self.sys_insertion_path = self.plugin_path else: from calibre.ptempfile import TemporaryDirectory self._sys_insertion_tdir = TemporaryDirectory('plugin_unzip') self.sys_insertion_path = self._sys_insertion_tdir.__enter__(*args) zf.extractall(self.sys_insertion_path) sys.path.append(self.sys_insertion_path) def __exit__(self, *args): ip, it = getattr(self, 'sys_insertion_path', None), getattr(self, '_sys_insertion_tdir', None) if ip in sys.path: sys.path.remove(ip) if hasattr(it, '__exit__'): it.__exit__(*args)

[documentos] def cli_main(self, args): ''' This method is the main entry point for your plugins command line interface. It is called when the user does: calibre-debug -r "Plugin Name". Any arguments passed are present in the args variable. ''' raise NotImplementedError('The %s plugin has no command line interface' %self.name)

# }}}

[documentos] class FileTypePlugin(Plugin): # {{{ ''' A plugin that is associated with a particular set of file types. ''' #: Set of file types for which this plugin should be run. #: Use '*' for all file types. #: For example: ``{'lit', 'mobi', 'prc'}`` file_types = set() #: If True, this plugin is run when books are added #: to the database on_import = False #: If True, this plugin is run after books are added #: to the database. In this case the postimport and postadd #: methods of the plugin are called. on_postimport = False #: If True, this plugin is run after a book is converted. #: In this case the postconvert method of the plugin is called. on_postconvert = False #: If True, this plugin is run after a book file is deleted #: from the database. In this case the postdelete method of #: the plugin is called. on_postdelete = False #: If True, this plugin is run just before a conversion on_preprocess = False #: If True, this plugin is run after conversion #: on the final file produced by the conversion output plugin. on_postprocess = False type = _('File type')

[documentos] def run(self, path_to_ebook): ''' Run the plugin. Must be implemented in subclasses. It should perform whatever modifications are required on the e-book and return the absolute path to the modified e-book. If no modifications are needed, it should return the path to the original e-book. If an error is encountered it should raise an Exception. The default implementation simply return the path to the original e-book. Note that the path to the original file (before any file type plugins are run, is available as self.original_path_to_file). The modified e-book file should be created with the :meth:`temporary_file` method. :param path_to_ebook: Absolute path to the e-book. :return: Absolute path to the modified e-book. ''' # Default implementation does nothing return path_to_ebook

[documentos] def postimport(self, book_id, book_format, db): ''' Called post import, i.e., after the book file has been added to the database. Note that this is different from :meth:`postadd` which is called when the book record is created for the first time. This method is called whenever a new file is added to a book record. It is useful for modifying the book record based on the contents of the newly added file. :param book_id: Database id of the added book. :param book_format: The file type of the book that was added. :param db: Library database. ''' pass # Default implementation does nothing

[documentos] def postconvert(self, book_id, book_format, db): ''' Called post conversion, i.e., after the conversion output book file has been added to the database. Note that it is run after a conversion only, not after a book is added. It is useful for modifying the book record based on the contents of the newly added file. :param book_id: Database id of the added book. :param book_format: The file type of the book that was added. :param db: Library database. ''' pass # Default implementation does nothing

[documentos] def postdelete(self, book_id, book_format, db): ''' Called post deletion, i.e., after the book file has been deleted from the database. Note that it is not run when a book record is deleted, only when one or more formats from the book are deleted. It is useful for modifying the book record based on the format of the deleted file. :param book_id: Database id of the added book. :param book_format: The file type of the book that was added. :param db: Library database. ''' pass # Default implementation does nothing

[documentos] def postadd(self, book_id, fmt_map, db): ''' Called post add, i.e. after a book has been added to the db. Note that this is different from :meth:`postimport`, which is called after a single book file has been added to a book. postadd() is called only when an entire book record with possibly more than one book file has been created for the first time. This is useful if you wish to modify the book record in the database when the book is first added to calibre. :param book_id: Database id of the added book. :param fmt_map: Map of file format to path from which the file format was added. Note that this might or might not point to an actual existing file, as sometimes files are added as streams. In which case it might be a dummy value or a non-existent path. :param db: Library database ''' pass # Default implementation does nothing

# }}}

[documentos] class MetadataReaderPlugin(Plugin): # {{{ ''' A plugin that implements reading metadata from a set of file types. ''' #: Set of file types for which this plugin should be run. #: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set() supported_platforms = ['windows', 'osx', 'linux'] version = numeric_version author = 'Kovid Goyal' type = _('Metadata reader') def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) self.quick = False

[documentos] def get_metadata(self, stream, type): ''' Return metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data. :param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`. :return: A :class:`calibre.ebooks.metadata.book.Metadata` object ''' return None

# }}}

[documentos] class MetadataWriterPlugin(Plugin): # {{{ ''' A plugin that implements reading metadata from a set of file types. ''' #: Set of file types for which this plugin should be run. #: For example: ``set(['lit', 'mobi', 'prc'])`` file_types = set() supported_platforms = ['windows', 'osx', 'linux'] version = numeric_version author = 'Kovid Goyal' type = _('Metadata writer') def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) self.apply_null = False

[documentos] def set_metadata(self, stream, mi, type): ''' Set metadata for the file represented by stream (a file like object that supports reading). Raise an exception when there is an error with the input data. :param type: The type of file. Guaranteed to be one of the entries in :attr:`file_types`. :param mi: A :class:`calibre.ebooks.metadata.book.Metadata` object ''' pass

# }}}

[documentos] class CatalogPlugin(Plugin): # {{{ ''' A plugin that implements a catalog generator. ''' resources_path = None #: Output file type for which this plugin should be run. #: For example: 'epub' or 'xml' file_types = set() type = _('Catalog generator') #: CLI parser options specific to this plugin, declared as `namedtuple` `Option`: #: #: from collections import namedtuple #: Option = namedtuple('Option', 'option, default, dest, help') #: cli_options = [Option('--catalog-title', default = 'My Catalog', #: dest = 'catalog_title', help = (_('Title of generated catalog. \nDefault:') + " '" + '%default' + "'"))] #: cli_options parsed in calibre.db.cli.cmd_catalog:option_parser() #: cli_options = [] def _field_sorter(self, key): ''' Custom fields sort after standard fields ''' if key.startswith('#'): return '~%s' % key[1:] else: return key def search_sort_db(self, db, opts): db.search(opts.search_text) if getattr(opts, 'sort_by', None): # 2nd arg = ascending db.sort(opts.sort_by, True) return db.get_data_as_dict(ids=opts.ids) def get_output_fields(self, db, opts): # Return a list of requested fields all_std_fields = {'author_sort','authors','comments','cover','formats', 'id','isbn','library_name','ondevice','pubdate','publisher', 'rating','series_index','series','size','tags','timestamp', 'title_sort','title','uuid','languages','identifiers'} all_custom_fields = set(db.custom_field_keys()) for field in list(all_custom_fields): fm = db.field_metadata[field] if fm['datatype'] == 'series': all_custom_fields.add(field+'_index') all_fields = all_std_fields.union(all_custom_fields) if getattr(opts, 'fields', 'all') != 'all': # Make a list from opts.fields of = [x.strip() for x in opts.fields.split(',')] requested_fields = set(of) # Validate requested_fields if requested_fields - all_fields: from calibre.library import current_library_name invalid_fields = sorted(list(requested_fields - all_fields)) print("invalid --fields specified: %s" % ', '.join(invalid_fields)) print("available fields in '%s': %s" % (current_library_name(), ', '.join(sorted(list(all_fields))))) raise ValueError("unable to generate catalog with specified fields") fields = [x for x in of if x in all_fields] else: fields = sorted(all_fields, key=self._field_sorter) if not opts.connected_device['is_device_connected'] and 'ondevice' in fields: fields.pop(int(fields.index('ondevice'))) return fields

[documentos] def initialize(self): ''' If plugin is not a built-in, copy the plugin's .ui and .py files from the ZIP file to $TMPDIR. Tab will be dynamically generated and added to the Catalog Options dialog in calibre.gui2.dialogs.catalog.py:Catalog ''' from calibre.customize.builtins import plugins as builtin_plugins from calibre.customize.ui import config from calibre.ptempfile import PersistentTemporaryDirectory if type(self) not in builtin_plugins and self.name not in config['disabled_plugins']: files_to_copy = [f"{self.name.lower()}.{ext}" for ext in ["ui","py"]] resources = zipfile.ZipFile(self.plugin_path,'r') if self.resources_path is None: self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='') for file in files_to_copy: try: resources.extract(file, self.resources_path) except: print(f" customize:__init__.initialize(): {file} not found in {os.path.basename(self.plugin_path)}") continue resources.close()

[documentos] def run(self, path_to_output, opts, db, ids, notification=None): ''' Run the plugin. Must be implemented in subclasses. It should generate the catalog in the format specified in file_types, returning the absolute path to the generated catalog file. If an error is encountered it should raise an Exception. The generated catalog file should be created with the :meth:`temporary_file` method. :param path_to_output: Absolute path to the generated catalog file. :param opts: A dictionary of keyword arguments :param db: A LibraryDatabase2 object ''' # Default implementation does nothing raise NotImplementedError('CatalogPlugin.generate_catalog() default ' 'method, should be overridden in subclass')

# }}}

[documentos] class InterfaceActionBase(Plugin): # {{{ supported_platforms = ['windows', 'osx', 'linux'] author = 'Kovid Goyal' type = _('User interface action') can_be_disabled = False actual_plugin = None def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) self.actual_plugin_ = None

[documentos] def load_actual_plugin(self, gui): ''' This method must return the actual interface action plugin object. ''' ac = self.actual_plugin_ if ac is None: mod, cls = self.actual_plugin.split(':') ac = getattr(importlib.import_module(mod), cls)(gui, self.site_customization) self.actual_plugin_ = ac return ac

# }}}

[documentos] class PreferencesPlugin(Plugin): # {{{ ''' A plugin representing a widget displayed in the Preferences dialog. This plugin has only one important method :meth:`create_widget`. The various fields of the plugin control how it is categorized in the UI. ''' supported_platforms = ['windows', 'osx', 'linux'] author = 'Kovid Goyal' type = _('Preferences') can_be_disabled = False #: Import path to module that contains a class named ConfigWidget #: which implements the ConfigWidgetInterface. Used by #: :meth:`create_widget`. config_widget = None #: Where in the list of categories the :attr:`category` of this plugin should be. category_order = 100 #: Where in the list of names in a category, the :attr:`gui_name` of this #: plugin should be name_order = 100 #: The category this plugin should be in category = None #: The category name displayed to the user for this plugin gui_category = None #: The name displayed to the user for this plugin gui_name = None #: The icon for this plugin, should be an absolute path icon = None #: The description used for tooltips and the like description = None

[documentos] def create_widget(self, parent=None): ''' Create and return the actual Qt widget used for setting this group of preferences. The widget must implement the :class:`calibre.gui2.preferences.ConfigWidgetInterface`. The default implementation uses :attr:`config_widget` to instantiate the widget. ''' base, _, wc = self.config_widget.partition(':') if not wc: wc = 'ConfigWidget' base = importlib.import_module(base) widget = getattr(base, wc) return widget(parent)

# }}}

class StoreBase(Plugin):  # {{{

    supported_platforms = ['windows', 'osx', 'linux']
    author         = 'John Schember'
    type = _('Store')
    # Information about the store. Should be in the primary language
    # of the store. This should not be translatable when set by
    # a subclass.
    description = _('An e-book store.')
    minimum_calibre_version = (0, 8, 0)
    version        = (1, 0, 1)

    actual_plugin = None

    # Does the store only distribute e-books without DRM.
    drm_free_only = False
    # This is the 2 letter country code for the corporate
    # headquarters of the store.
    headquarters = ''
    # All formats the store distributes e-books in.
    formats = []
    # Is this store on an affiliate program?
    affiliate = False

    def load_actual_plugin(self, gui):
        '''
        This method must return the actual interface action plugin object.
        '''
        mod, cls = self.actual_plugin.split(':')
        self.actual_plugin_object  = getattr(importlib.import_module(mod), cls)(gui, self.name)
        return self.actual_plugin_object

    def customization_help(self, gui=False):
        if getattr(self, 'actual_plugin_object', None) is not None:
            return self.actual_plugin_object.customization_help(gui)
        raise NotImplementedError()

    def config_widget(self):
        if getattr(self, 'actual_plugin_object', None) is not None:
            return self.actual_plugin_object.config_widget()
        raise NotImplementedError()

    def save_settings(self, config_widget):
        if getattr(self, 'actual_plugin_object', None) is not None:
            return self.actual_plugin_object.save_settings(config_widget)
        raise NotImplementedError()

# }}}

class EditBookToolPlugin(Plugin):  # {{{

    type = _('Edit book tool')
    minimum_calibre_version = (1, 46, 0)

# }}}

class LibraryClosedPlugin(Plugin):  # {{{
    '''
    LibraryClosedPlugins are run when a library is closed, either at shutdown,
    when the library is changed, or when a library is used in some other way.
    At the moment these plugins won't be called by the CLI functions.
    '''
    type = _('Library closed')

    # minimum version 2.54 because that is when support was added
    minimum_calibre_version = (2, 54, 0)

    def run(self, db):
        '''
        The db will be a reference to the new_api (db.cache.py).

        The plugin must run to completion. It must not use the GUI, threads, or
        any signals.
        '''
        raise NotImplementedError('LibraryClosedPlugin '
                'run method must be overridden in subclass')
# }}}

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.customize »

calibre.customize.conversion

Código fuente para calibre.customize.conversion

'''
Defines the plugin system for conversions.
'''
import numbers
import os
import re
import shutil

from calibre import CurrentDir
from calibre.customize import Plugin

class ConversionOption:

    '''
    Class representing conversion options
    '''

    def __init__(self, name=None, help=None, long_switch=None,
                 short_switch=None, choices=None):
        self.name = name
        self.help = help
        self.long_switch = long_switch
        self.short_switch = short_switch
        self.choices = choices

        if self.long_switch is None:
            self.long_switch = self.name.replace('_', '-')

        self.validate_parameters()

    def validate_parameters(self):
        '''
        Validate the parameters passed to :meth:`__init__`.
        '''
        if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
            raise ValueError(self.name + ' is not a valid Python identifier')
        if not self.help:
            raise ValueError('You must set the help text')

    def __hash__(self):
        return hash(self.name)

    def __eq__(self, other):
        return self.name == getattr(other, 'name', other)

    def clone(self):
        return ConversionOption(name=self.name, help=self.help,
                long_switch=self.long_switch, short_switch=self.short_switch,
                choices=self.choices)

class OptionRecommendation:
    LOW  = 1
    MED  = 2
    HIGH = 3

    def __init__(self, recommended_value=None, level=LOW, **kwargs):
        '''
        An option recommendation. That is, an option as well as its recommended
        value and the level of the recommendation.
        '''
        self.level = level
        self.recommended_value = recommended_value
        self.option = kwargs.pop('option', None)
        if self.option is None:
            self.option = ConversionOption(**kwargs)

        self.validate_parameters()

    @property
    def help(self):
        return self.option.help

    def clone(self):
        return OptionRecommendation(recommended_value=self.recommended_value,
                level=self.level, option=self.option.clone())

    def validate_parameters(self):
        if self.option.choices and self.recommended_value not in \
                                                    self.option.choices:
            raise ValueError('OpRec: %s: Recommended value not in choices'%
                             self.option.name)
        if not (isinstance(self.recommended_value, (numbers.Number, bytes, str)) or self.recommended_value is None):
            raise ValueError('OpRec: %s:'%self.option.name + repr(
                self.recommended_value) + ' is not a string or a number')

class DummyReporter:

    def __init__(self):
        self.cancel_requested = False

    def __call__(self, percent, msg=''):
        pass

def gui_configuration_widget(name, parent, get_option_by_name,
        get_option_help, db, book_id, for_output=True):
    import importlib

    def widget_factory(cls):
        return cls(parent, get_option_by_name,
            get_option_help, db, book_id)

    if for_output:
        try:
            output_widget = importlib.import_module(
                    'calibre.gui2.convert.'+name)
            pw = output_widget.PluginWidget
            pw.ICON = 'back.png'
            pw.HELP = _('Options specific to the output format.')
            return widget_factory(pw)
        except ImportError:
            pass
    else:
        try:
            input_widget = importlib.import_module(
                    'calibre.gui2.convert.'+name)
            pw = input_widget.PluginWidget
            pw.ICON = 'forward.png'
            pw.HELP = _('Options specific to the input format.')
            return widget_factory(pw)
        except ImportError:
            pass
    return None

[documentos] class InputFormatPlugin(Plugin): ''' InputFormatPlugins are responsible for converting a document into HTML+OPF+CSS+etc. The results of the conversion *must* be encoded in UTF-8. The main action happens in :meth:`convert`. ''' type = _('Conversion input') can_be_disabled = False supported_platforms = ['windows', 'osx', 'linux'] commit_name = None # unique name under which options for this plugin are saved ui_data = None #: Set of file types for which this plugin should be run #: For example: ``set(['azw', 'mobi', 'prc'])`` file_types = set() #: If True, this input plugin generates a collection of images, #: one per HTML file. This can be set dynamically, in the convert method #: if the input files can be both image collections and non-image collections. #: If you set this to True, you must implement the get_images() method that returns #: a list of images. is_image_collection = False #: Number of CPU cores used by this plugin. #: A value of -1 means that it uses all available cores core_usage = 1 #: If set to True, the input plugin will perform special processing #: to make its output suitable for viewing for_viewer = False #: The encoding that this input plugin creates files in. A value of #: None means that the encoding is undefined and must be #: detected individually output_encoding = 'utf-8' #: Options shared by all Input format plugins. Do not override #: in sub-classes. Use :attr:`options` instead. Every option must be an #: instance of :class:`OptionRecommendation`. common_options = { OptionRecommendation(name='input_encoding', recommended_value=None, level=OptionRecommendation.LOW, help=_('Specify the character encoding of the input document. If ' 'set this option will override any encoding declared by the ' 'document itself. Particularly useful for documents that ' 'do not declare an encoding or that have erroneous ' 'encoding declarations.') )} #: Options to customize the behavior of this plugin. Every option must be an #: instance of :class:`OptionRecommendation`. options = set() #: A set of 3-tuples of the form #: (option_name, recommended_value, recommendation_level) recommendations = set() def __init__(self, *args): Plugin.__init__(self, *args) self.report_progress = DummyReporter()

[documentos] def get_images(self): ''' Return a list of absolute paths to the images, if this input plugin represents an image collection. The list of images is in the same order as the spine and the TOC. ''' raise NotImplementedError()

[documentos] def convert(self, stream, options, file_ext, log, accelerators): ''' This method must be implemented in sub-classes. It must return the path to the created OPF file or an :class:`OEBBook` instance. All output should be contained in the current folder. If this plugin creates files outside the current folder they must be deleted/marked for deletion before this method returns. :param stream: A file like object that contains the input file. :param options: Options to customize the conversion process. Guaranteed to have attributes corresponding to all the options declared by this plugin. In addition, it will have a verbose attribute that takes integral values from zero upwards. Higher numbers mean be more verbose. Another useful attribute is ``input_profile`` that is an instance of :class:`calibre.customize.profiles.InputProfile`. :param file_ext: The extension (without the .) of the input file. It is guaranteed to be one of the `file_types` supported by this plugin. :param log: A :class:`calibre.utils.logging.Log` object. All output should use this object. :param accelarators: A dictionary of various information that the input plugin can get easily that would speed up the subsequent stages of the conversion. ''' raise NotImplementedError()

def __call__(self, stream, options, file_ext, log, accelerators, output_dir): try: log('InputFormatPlugin: %s running'%self.name) if hasattr(stream, 'name'): log('on', stream.name) except: # In case stdout is broken pass with CurrentDir(output_dir): for x in os.listdir('.'): shutil.rmtree(x) if os.path.isdir(x) else os.remove(x) ret = self.convert(stream, options, file_ext, log, accelerators) return ret

[documentos] def postprocess_book(self, oeb, opts, log): ''' Called to allow the input plugin to perform postprocessing after the book has been parsed. ''' pass

[documentos] def specialize(self, oeb, opts, log, output_fmt): ''' Called to allow the input plugin to specialize the parsed book for a particular output format. Called after postprocess_book and before any transforms are performed on the parsed book. ''' pass

[documentos] def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None): ''' Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class. See the builtin input plugins for examples. ''' name = self.name.lower().replace(' ', '_') return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=False)

[documentos] class OutputFormatPlugin(Plugin): ''' OutputFormatPlugins are responsible for converting an OEB document (OPF+HTML) into an output e-book. The OEB document can be assumed to be encoded in UTF-8. The main action happens in :meth:`convert`. ''' type = _('Conversion output') can_be_disabled = False supported_platforms = ['windows', 'osx', 'linux'] commit_name = None # unique name under which options for this plugin are saved ui_data = None #: The file type (extension without leading period) that this #: plugin outputs file_type = None #: Options shared by all Input format plugins. Do not override #: in sub-classes. Use :attr:`options` instead. Every option must be an #: instance of :class:`OptionRecommendation`. common_options = { OptionRecommendation(name='pretty_print', recommended_value=False, level=OptionRecommendation.LOW, help=_('If specified, the output plugin will try to create output ' 'that is as human readable as possible. May not have any effect ' 'for some output plugins.') )} #: Options to customize the behavior of this plugin. Every option must be an #: instance of :class:`OptionRecommendation`. options = set() #: A set of 3-tuples of the form #: (option_name, recommended_value, recommendation_level) recommendations = set() @property def description(self): return _('Convert e-books to the %s format')%self.file_type.upper() def __init__(self, *args): Plugin.__init__(self, *args) self.report_progress = DummyReporter()

[documentos] def convert(self, oeb_book, output, input_plugin, opts, log): ''' Render the contents of `oeb_book` (which is an instance of :class:`calibre.ebooks.oeb.OEBBook`) to the file specified by output. :param output: Either a file like object or a string. If it is a string it is the path to a folder that may or may not exist. The output plugin should write its output into that folder. If it is a file like object, the output plugin should write its output into the file. :param input_plugin: The input plugin that was used at the beginning of the conversion pipeline. :param opts: Conversion options. Guaranteed to have attributes corresponding to the OptionRecommendations of this plugin. :param log: The logger. Print debug/info messages etc. using this. ''' raise NotImplementedError()

@property def is_periodical(self): return self.oeb.metadata.publication_type and \ str(self.oeb.metadata.publication_type[0]).startswith('periodical:')

[documentos] def specialize_options(self, log, opts, input_fmt): ''' Can be used to change the values of conversion options, as used by the conversion pipeline. ''' pass

[documentos] def specialize_css_for_output(self, log, opts, item, stylizer): ''' Can be used to make changes to the CSS during the CSS flattening process. :param item: The item (HTML file) being processed :param stylizer: A Stylizer object containing the flattened styles for item. You can get the style for any element by stylizer.style(element). ''' pass

[documentos] def gui_configuration_widget(self, parent, get_option_by_name, get_option_help, db, book_id=None): ''' Called to create the widget used for configuring this plugin in the calibre GUI. The widget must be an instance of the PluginWidget class. See the builtin output plugins for examples. ''' name = self.name.lower().replace(' ', '_') return gui_configuration_widget(name, parent, get_option_by_name, get_option_help, db, book_id, for_output=True)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.db.cache

Código fuente para calibre.db.cache

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import hashlib
import operator
import os
import random
import shutil
import sys
import traceback
import weakref
from collections import defaultdict
from collections.abc import MutableSet, Set
from contextlib import closing
from functools import partial, wraps
from io import DEFAULT_BUFFER_SIZE, BytesIO
from queue import Queue
from threading import Lock
from time import mktime, monotonic, sleep, time
from typing import NamedTuple, Optional, Tuple

from calibre import as_unicode, detect_ncpus, isbytestring
from calibre.constants import iswindows, preferred_encoding
from calibre.customize.ui import run_plugins_on_import, run_plugins_on_postadd, run_plugins_on_postdelete, run_plugins_on_postimport
from calibre.db import SPOOL_SIZE, _get_next_series_num_for_list
from calibre.db.annotations import merge_annotations
from calibre.db.categories import get_categories
from calibre.db.constants import NOTES_DIR_NAME
from calibre.db.errors import NoSuchBook, NoSuchFormat
from calibre.db.fields import IDENTITY, InvalidLinkTable, create_field
from calibre.db.lazy import FormatMetadata, FormatsList, ProxyMetadata
from calibre.db.listeners import EventDispatcher, EventType
from calibre.db.locking import DowngradeLockError, LockingError, SafeReadLock, create_locks, try_lock
from calibre.db.notes.connect import copy_marked_up_text
from calibre.db.search import Search
from calibre.db.tables import VirtualTable
from calibre.db.utils import type_safe_sort_key_function
from calibre.db.write import get_series_values, uniq
from calibre.ebooks import check_ebook_format
from calibre.ebooks.metadata import author_to_author_sort, string_to_authors
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile, SpooledTemporaryFile, base_dir
from calibre.utils.config import prefs, tweaks
from calibre.utils.date import UNDEFINED_DATE, utcnow
from calibre.utils.date import now as nowf
from calibre.utils.filenames import make_long_path_useable
from calibre.utils.icu import lower as icu_lower
from calibre.utils.icu import sort_key
from calibre.utils.localization import canonicalize_lang
from polyglot.builtins import cmp, iteritems, itervalues, string_or_bytes

class ExtraFile(NamedTuple):
    relpath: str
    file_path: str
    stat_result: os.stat_result

def api(f):
    f.is_cache_api = True
    return f

def read_api(f):
    f = api(f)
    f.is_read_api = True
    return f

def write_api(f):
    f = api(f)
    f.is_read_api = False
    return f

def wrap_simple(lock, func):
    @wraps(func)
    def call_func_with_lock(*args, **kwargs):
        try:
            with lock:
                return func(*args, **kwargs)
        except DowngradeLockError:
            # We already have an exclusive lock, no need to acquire a shared
            # lock. See the safe_read_lock properties' documentation for why
            # this is necessary.
            return func(*args, **kwargs)
    return call_func_with_lock

def run_import_plugins(path_or_stream, fmt):
    fmt = fmt.lower()
    if hasattr(path_or_stream, 'seek'):
        path_or_stream.seek(0)
        pt = PersistentTemporaryFile('_import_plugin.'+fmt)
        shutil.copyfileobj(path_or_stream, pt, 1024**2)
        pt.close()
        path = pt.name
    else:
        path = path_or_stream
    return run_plugins_on_import(path, fmt)

def _add_newbook_tag(mi):
    tags = prefs['new_book_tags']
    if tags:
        for tag in [t.strip() for t in tags]:
            if tag:
                if not mi.tags:
                    mi.tags = [tag]
                elif tag not in mi.tags:
                    mi.tags.append(tag)

def _add_default_custom_column_values(mi, fm):
    cols = fm.custom_field_metadata(include_composites=False)
    for cc,col in iteritems(cols):
        dv = col['display'].get('default_value', None)
        try:
            if dv is not None:
                if not mi.get_user_metadata(cc, make_copy=False):
                    mi.set_user_metadata(cc, col)
                dt = col['datatype']
                if dt == 'datetime' and icu_lower(dv) == 'now':
                    dv = nowf()
                mi.set(cc, dv)
        except:
            traceback.print_exc()

dynamic_category_preferences = frozenset({'grouped_search_make_user_categories', 'grouped_search_terms', 'user_categories'})

[documentos] class Cache: ''' An in-memory cache of the metadata.db file from a calibre library. This class also serves as a threadsafe API for accessing the database. The in-memory cache is maintained in normal form for maximum performance. SQLITE is simply used as a way to read and write from metadata.db robustly. All table reading/sorting/searching/caching logic is re-implemented. This was necessary for maximum performance and flexibility. ''' EventType = EventType fts_indexing_sleep_time = 4 # seconds def __init__(self, backend, library_database_instance=None): self.shutting_down = False self.is_doing_rebuild_or_vacuum = False self.backend = backend self.library_database_instance = (None if library_database_instance is None else weakref.ref(library_database_instance)) self.event_dispatcher = EventDispatcher() self.fields = {} self.composites = {} self.read_lock, self.write_lock = create_locks() self.format_metadata_cache = defaultdict(dict) self.formatter_template_cache = {} self.dirtied_cache = {} self.link_maps_cache = {} self.extra_files_cache = {} self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None self.vls_cache_lock = Lock() self.dirtied_sequence = 0 self.cover_caches = set() self.clear_search_cache_count = 0 # Implement locking for all simple read/write API methods # An unlocked version of the method is stored with the name starting # with a leading underscore. Use the unlocked versions when the lock # has already been acquired. for name in dir(self): func = getattr(self, name) ira = getattr(func, 'is_read_api', None) if ira is not None: # Save original function setattr(self, '_'+name, func) # Wrap it in a lock lock = self.read_lock if ira else self.write_lock setattr(self, name, wrap_simple(lock, func)) self._search_api = Search(self, 'saved_searches', self.field_metadata.get_search_terms()) self.initialize_dynamic() self.initialize_fts() @property def new_api(self): return self @property def library_id(self): return self.backend.library_id @property def dbpath(self): return self.backend.dbpath @property def is_fat_filesystem(self): return self.backend.is_fat_filesystem @property def safe_read_lock(self): ''' A safe read lock is a lock that does nothing if the thread already has a write lock, otherwise it acquires a read lock. This is necessary to prevent DowngradeLockErrors, which can happen when updating the search cache in the presence of composite columns. Updating the search cache holds an exclusive lock, but searching a composite column involves reading field values via ProxyMetadata which tries to get a shared lock. There may be other scenarios that trigger this as well. This property returns a new lock object on every access. This lock object is not recursive (for performance) and must only be used in a with statement as ``with cache.safe_read_lock:`` otherwise bad things will happen.''' return SafeReadLock(self.read_lock) @write_api def ensure_has_search_category(self, fail_on_existing=True): if len(self._search_api.saved_searches.names()) > 0: self.field_metadata.add_search_category(label='search', name=_('Saved searches'), fail_on_existing=fail_on_existing) def _initialize_dynamic_categories(self): # Reconstruct the user categories, putting them into field_metadata fm = self.field_metadata fm.remove_dynamic_categories() for user_cat in sorted(self._pref('user_categories', {}), key=sort_key): cat_name = '@' + user_cat # add the '@' to avoid name collision while cat_name: try: fm.add_user_category(label=cat_name, name=user_cat) except ValueError: break # Can happen since we are removing dots and adding parent categories ourselves cat_name = cat_name.rpartition('.')[0] # add grouped search term user categories muc = frozenset(self._pref('grouped_search_make_user_categories', [])) for cat in sorted(self._pref('grouped_search_terms', {}), key=sort_key): if cat in muc: # There is a chance that these can be duplicates of an existing # user category. Print the exception and continue. try: self.field_metadata.add_user_category(label='@' + cat, name=cat) except ValueError: traceback.print_exc() self._ensure_has_search_category() self.field_metadata.add_grouped_search_terms( self._pref('grouped_search_terms', {})) self._refresh_search_locations() @write_api def initialize_dynamic(self): self.backend.dirty_books_with_dirtied_annotations() self.dirtied_cache = {x:i for i, x in enumerate(self.backend.dirtied_books())} if self.dirtied_cache: self.dirtied_sequence = max(itervalues(self.dirtied_cache))+1 self._initialize_dynamic_categories() @write_api def initialize_template_cache(self): self.formatter_template_cache = {} @write_api def set_user_template_functions(self, user_template_functions): self.backend.set_user_template_functions(user_template_functions) @write_api def clear_composite_caches(self, book_ids=None): for field in itervalues(self.composites): field.clear_caches(book_ids=book_ids) @write_api def clear_search_caches(self, book_ids=None): self.clear_search_cache_count += 1 self._search_api.update_or_clear(self, book_ids) self.vls_for_books_cache = None self.vls_for_books_lib_in_process = None @write_api def clear_extra_files_cache(self, book_id=None): if book_id is None: self.extra_files_cache = {} else: self.extra_files_cache.pop(book_id, None) @read_api def last_modified(self): return self.backend.last_modified() @write_api def clear_caches(self, book_ids=None, template_cache=True, search_cache=True): if template_cache: self._initialize_template_cache() # Clear the formatter template cache for field in itervalues(self.fields): if hasattr(field, 'clear_caches'): field.clear_caches(book_ids=book_ids) # Clear the composite cache and ondevice caches if book_ids: for book_id in book_ids: self.format_metadata_cache.pop(book_id, None) else: self.format_metadata_cache.clear() if search_cache: self._clear_search_caches(book_ids) self._clear_link_map_cache(book_ids) @write_api def clear_link_map_cache(self, book_ids=None): if book_ids is None: self.link_maps_cache = {} else: for book in book_ids: self.link_maps_cache.pop(book, None) @write_api def reload_from_db(self, clear_caches=True): if clear_caches: self._clear_caches() with self.backend.conn: # Prevent other processes, such as calibredb from interrupting the reload by locking the db self.backend.prefs.load_from_db() self._search_api.saved_searches.load_from_db() for field in itervalues(self.fields): if hasattr(field, 'table'): field.table.read(self.backend) # Reread data from metadata.db @property def field_metadata(self): return self.backend.field_metadata def _get_metadata(self, book_id, get_user_categories=True): # {{{ mi = Metadata(None, template_cache=self.formatter_template_cache) mi._proxy_metadata = ProxyMetadata(self, book_id, formatter=mi.formatter) author_ids = self._field_ids_for('authors', book_id) adata = self._author_data(author_ids) aut_list = [adata[i] for i in author_ids] aum = [] aus = {} for rec in aut_list: aut = rec['name'] aum.append(aut) aus[aut] = rec['sort'] mi.title = self._field_for('title', book_id, default_value=_('Unknown')) mi.authors = aum mi.author_sort = self._field_for('author_sort', book_id, default_value=_('Unknown')) mi.author_sort_map = aus mi.comments = self._field_for('comments', book_id) mi.publisher = self._field_for('publisher', book_id) n = utcnow() mi.timestamp = self._field_for('timestamp', book_id, default_value=n) mi.pubdate = self._field_for('pubdate', book_id, default_value=n) mi.uuid = self._field_for('uuid', book_id, default_value='dummy') mi.title_sort = self._field_for('sort', book_id, default_value=_('Unknown')) mi.last_modified = self._field_for('last_modified', book_id, default_value=n) formats = self._field_for('formats', book_id) mi.format_metadata = {} mi.languages = list(self._field_for('languages', book_id)) if not formats: good_formats = None else: mi.format_metadata = FormatMetadata(self, book_id, formats) good_formats = FormatsList(sorted(formats), mi.format_metadata) # These three attributes are returned by the db2 get_metadata(), # however, we dont actually use them anywhere other than templates, so # they have been removed, to avoid unnecessary overhead. The templates # all use _proxy_metadata. # mi.book_size = self._field_for('size', book_id, default_value=0) # mi.ondevice_col = self._field_for('ondevice', book_id, default_value='') # mi.db_approx_formats = formats mi.formats = good_formats mi.has_cover = _('Yes') if self._field_for('cover', book_id, default_value=False) else '' mi.tags = list(self._field_for('tags', book_id, default_value=())) mi.series = self._field_for('series', book_id) if mi.series: mi.series_index = self._field_for('series_index', book_id, default_value=1.0) mi.rating = self._field_for('rating', book_id) mi.set_identifiers(self._field_for('identifiers', book_id, default_value={})) mi.application_id = book_id mi.id = book_id for key, meta in self.field_metadata.custom_iteritems(): mi.set_user_metadata(key, meta) if meta['datatype'] != 'composite': # composites are evaluated on demand in metadata.book.base # because their value is None val = self._field_for(key, book_id) if isinstance(val, tuple): val = list(val) extra = self._field_for(key+'_index', book_id) mi.set(key, val=val, extra=extra) mi.link_maps = self._get_all_link_maps_for_book(book_id) user_cat_vals = {} if get_user_categories: user_cats = self._pref('user_categories', {}) for ucat in user_cats: res = [] for name,cat,ign in user_cats[ucat]: v = mi.get(cat, None) if isinstance(v, list): if name in v: res.append([name,cat]) elif name == v: res.append([name,cat]) user_cat_vals[ucat] = res mi.user_categories = user_cat_vals return mi # }}}

[documentos] @api def init(self): ''' Initialize this cache with data from the backend. ''' with self.write_lock: self.backend.read_tables() bools_are_tristate = self.backend.prefs['bools_are_tristate'] for field, table in iteritems(self.backend.tables): self.fields[field] = create_field(field, table, bools_are_tristate, self.backend.get_template_functions) if table.metadata['datatype'] == 'composite': self.composites[field] = self.fields[field] self.fields['ondevice'] = create_field('ondevice', VirtualTable('ondevice'), bools_are_tristate, self.backend.get_template_functions) for name, field in iteritems(self.fields): if name[0] == '#' and name.endswith('_index'): field.series_field = self.fields[name[:-len('_index')]] self.fields[name[:-len('_index')]].index_field = field elif name == 'series_index': field.series_field = self.fields['series'] self.fields['series'].index_field = field elif name == 'authors': field.author_sort_field = self.fields['author_sort'] elif name == 'title': field.title_sort_field = self.fields['sort'] if self.backend.prefs['update_all_last_mod_dates_on_start']: self.update_last_modified(self.all_book_ids()) self.backend.prefs.set('update_all_last_mod_dates_on_start', False)

# FTS API {{{ def initialize_fts(self): self.fts_queue_thread = None self.fts_measuring_rate = None self.fts_num_done_since_start = 0 self.fts_job_queue = Queue() self.fts_indexing_left = self.fts_indexing_total = 0 fts = self.backend.initialize_fts(weakref.ref(self)) if self.is_fts_enabled(): self.start_fts_pool() return fts def start_fts_pool(self): from threading import Event, Thread self.fts_dispatch_stop_event = Event() self.fts_queue_thread = Thread(name='FTSQueue', target=Cache.dispatch_fts_jobs, args=( self.fts_job_queue, self.fts_dispatch_stop_event, weakref.ref(self)), daemon=True) self.fts_queue_thread.start() self.backend.fts.pool.initialize() self.backend.fts.pool.initialized.wait() self.queue_next_fts_job() @read_api def is_fts_enabled(self): return self.backend.fts_enabled @write_api def fts_start_measuring_rate(self, measure=True): self.fts_measuring_rate = monotonic() if measure else None self.fts_num_done_since_start = 0 def _update_fts_indexing_numbers(self, job_time=None): # this is called when new formats are added and when a format is # indexed, but NOT when books or formats are deleted, so total may not # be up to date. nl = self.backend.fts.number_dirtied() nt = self.backend.get('SELECT COUNT(*) FROM main.data')[0][0] or 0 if not nl: self._fts_start_measuring_rate(measure=False) if job_time is not None and self.fts_measuring_rate is not None: self.fts_num_done_since_start += 1 if (self.fts_indexing_left, self.fts_indexing_total) != (nl, nt) or job_time is not None: self.fts_indexing_left = nl self.fts_indexing_total = nt self.event_dispatcher(EventType.indexing_progress_changed, *self._fts_indexing_progress()) @read_api def fts_indexing_progress(self): rate = None if self.fts_measuring_rate is not None and self.fts_num_done_since_start > 4: rate = self.fts_num_done_since_start / (monotonic() - self.fts_measuring_rate) return self.fts_indexing_left, self.fts_indexing_total, rate @write_api def enable_fts(self, enabled=True, start_pool=True): fts = self.backend.enable_fts(weakref.ref(self) if enabled else None) if fts and start_pool: # used in the tests self.start_fts_pool() if not fts and self.fts_queue_thread: self.fts_job_queue.put(None) self.fts_queue_thread = None self.fts_job_queue = Queue() if fts: self._update_fts_indexing_numbers() return fts @write_api def fts_unindex(self, book_id, fmt=None): self.backend.fts_unindex(book_id, fmt=fmt) @staticmethod def dispatch_fts_jobs(queue, stop_dispatch, dbref): from .fts.text import is_fmt_ok def do_one(): self = dbref() if self is None: return False start_time = monotonic() with self.read_lock: if not self.backend.fts_enabled: return False book_id, fmt = self.backend.get_next_fts_job() if book_id is None: return False path = self._format_abspath(book_id, fmt) if not path or not is_fmt_ok(fmt): with self.write_lock: self.backend.remove_dirty_fts(book_id, fmt) self._update_fts_indexing_numbers() return True with self.read_lock, open(path, 'rb') as src, PersistentTemporaryFile(suffix=f'.{fmt.lower()}') as pt: sz = 0 h = hashlib.sha1() while True: chunk = src.read(DEFAULT_BUFFER_SIZE) if not chunk: break sz += len(chunk) h.update(chunk) pt.write(chunk) with self.write_lock: queued = self.backend.queue_fts_job(book_id, fmt, pt.name, sz, h.hexdigest(), start_time) if not queued: # means a dirtied book was removed from the dirty list because the text has not changed self._update_fts_indexing_numbers(monotonic() - start_time) return self.backend.fts_has_idle_workers def loop_while_more_available(): self = dbref() if not self or not self.backend.fts_enabled: return has_more = True while has_more and not self.shutting_down and self.backend.fts_enabled and not stop_dispatch.is_set(): try: has_more = do_one() except Exception: if self.backend.fts_enabled: traceback.print_exc() sleep(self.fts_indexing_sleep_time) while not getattr(dbref(), 'shutting_down', True): x = queue.get() if x is None: break loop_while_more_available() @write_api def queue_next_fts_job(self): if not self.backend.fts_enabled: return self.fts_job_queue.put(True) self._update_fts_indexing_numbers() @write_api def commit_fts_result(self, book_id, fmt, fmt_size, fmt_hash, text, err_msg, start_time): ans = self.backend.commit_fts_result(book_id, fmt, fmt_size, fmt_hash, text, err_msg) self._update_fts_indexing_numbers(monotonic() - start_time) return ans @write_api def reindex_fts_book(self, book_id, *fmts): if not self.is_fts_enabled(): return if not fmts: fmts = self._formats(book_id) self.backend.reindex_fts_book(book_id, *fmts) self._queue_next_fts_job() @api def reindex_fts(self): if not self.is_fts_enabled(): return with self.write_lock: self._shutdown_fts() self._shutdown_fts(stage=2) with self.write_lock: self.backend.reindex_fts() fts = self.initialize_fts() fts.initialize(self.backend.conn) # ensure fts is pre-initialized needed for the tests self._queue_next_fts_job() return fts @write_api def set_fts_num_of_workers(self, num): existing = self.backend.fts_num_of_workers if num != existing: self.backend.fts_num_of_workers = num if num > existing: self._queue_next_fts_job() return True return False @write_api def set_fts_speed(self, slow=True): orig = self.fts_indexing_sleep_time if slow: self.fts_indexing_sleep_time = Cache.fts_indexing_sleep_time changed = self._set_fts_num_of_workers(1) else: self.fts_indexing_sleep_time = 0.1 changed = self._set_fts_num_of_workers(max(1, detect_ncpus())) changed = changed or orig != self.fts_indexing_sleep_time if changed and self.fts_measuring_rate is not None: self._fts_start_measuring_rate() return changed @write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def fts_search( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_book_ids=None, return_text=True, result_type=tuple, process_each_result=None, ): return result_type(self.backend.fts_search( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_book_ids=restrict_to_book_ids, process_each_result=process_each_result, )) # }}} # Notes API {{{

[documentos] @read_api def notes_for(self, field, item_id) -> str: ' Return the notes document or an empty string if not found ' return self.backend.notes_for(field, item_id)

[documentos] @read_api def notes_data_for(self, field, item_id) -> str: ' Return all notes data as a dict or None if note does not exist ' return self.backend.notes_data_for(field, item_id)

[documentos] @read_api def get_all_items_that_have_notes(self, field_name=None) -> set[int] | dict[str, set[int]]: ' Return all item_ids for items that have notes in the specified field or all fields if field_name is None ' return self.backend.get_all_items_that_have_notes(field_name)

[documentos] @read_api def field_supports_notes(self, field=None) -> bool: ' Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes. ' if field is None: return self.backend.notes.allowed_fields return field in self.backend.notes.allowed_fields

[documentos] @read_api def items_with_notes_in_book(self, book_id: int) -> dict[str, dict[int, str]]: ' Return a dict of field to items that have associated notes for that field for the specified book ' ans = {} for k in self.backend.notes.allowed_fields: try: field = self.fields[k] except KeyError: continue v = {} for item_id in field.ids_for_book(book_id): if self.backend.notes_for(k, item_id): v[item_id] = field.table.id_map[item_id] if v: ans[k] = v return ans

[documentos] @write_api def set_notes_for(self, field, item_id, doc: str, searchable_text: str = copy_marked_up_text, resource_hashes=(), remove_unused_resources=False) -> int: ''' Set the notes document. If the searchable text is different from the document, specify it as searchable_text. If the document references resources their hashes must be present in resource_hashes. Set remove_unused_resources to True to cleanup unused resources, note that updating a note automatically cleans up resources pertaining to that note anyway. ''' return self.backend.set_notes_for(field, item_id, doc, searchable_text, resource_hashes, remove_unused_resources)

[documentos] @write_api def add_notes_resource(self, path_or_stream_or_data, name: str, mtime: float = None) -> int: ' Add the specified resource so it can be referenced by notes and return its content hash ' return self.backend.add_notes_resource(path_or_stream_or_data, name, mtime)

[documentos] @read_api def get_notes_resource(self, resource_hash) -> Optional[dict]: ' Return a dict containing the resource data and name or None if no resource with the specified hash is found ' return self.backend.get_notes_resource(resource_hash)

[documentos] @read_api def notes_resources_used_by(self, field, item_id): ' Return the set of resource hashes of all resources used by the note for the specified item ' return frozenset(self.backend.notes_resources_used_by(field, item_id))

[documentos] @write_api def unretire_note_for(self, field, item_id) -> int: ' Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database ' return self.backend.unretire_note_for(field, item_id)

[documentos] @read_api def export_note(self, field, item_id) -> str: ' Export the note as a single HTML document with embedded images as data: URLs ' return self.backend.export_note(field, item_id)

[documentos] @write_api def import_note(self, field, item_id, path_to_html_file, path_is_data=False): ' Import a previously exported note or an arbitrary HTML file as the note for the specified item ' if path_is_data: html = path_to_html_file ctime = mtime = time() basedir = base_dir() else: with open(path_to_html_file, 'rb') as f: html = f.read() st = os.stat(f.fileno()) ctime, mtime = st.st_ctime, st.st_mtime basedir = os.path.dirname(os.path.abspath(path_to_html_file)) return self.backend.import_note(field, item_id, html, basedir, ctime, mtime)

[documentos] @write_api # we need to use write locking as SQLITE gives a locked table error if multiple FTS queries are made at the same time def search_notes( self, fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=tuple, process_each_result=None, limit=None, ): ' Search the text of notes using an FTS index. If the query is empty return all notes. ' return result_type(self.backend.search_notes( fts_engine_query, use_stemming=use_stemming, highlight_start=highlight_start, highlight_end=highlight_end, snippet_size=snippet_size, return_text=return_text, restrict_to_fields=restrict_to_fields, process_each_result=process_each_result, limit=limit, ))

# }}} # Cache Layer API {{{

[documentos] @write_api def add_listener(self, event_callback_function, check_already_added=False): ''' Register a callback function that will be called after certain actions are taken on this database. The function must take three arguments: (:class:`EventType`, library_id, event_type_specific_data) ''' self.event_dispatcher.library_id = getattr(self, 'server_library_id', self.library_id) if check_already_added and event_callback_function in self.event_dispatcher: return False self.event_dispatcher.add_listener(event_callback_function) return True

@write_api def remove_listener(self, event_callback_function): self.event_dispatcher.remove_listener(event_callback_function)

[documentos] @read_api def field_for(self, name, book_id, default_value=None): ''' Return the value of the field ``name`` for the book identified by ``book_id``. If no such book exists or it has no defined value for the field ``name`` or no such field exists, then ``default_value`` is returned. ``default_value`` is not used for title, title_sort, authors, author_sort and series_index. This is because these always have values in the db. ``default_value`` is used for all custom columns. The returned value for is_multiple fields are always tuples, even when no values are found (in other words, default_value is ignored). The exception is identifiers for which the returned value is always a dictionary. The returned tuples are always in link order, that is, the order in which they were created. ''' if self.composites and name in self.composites: return self.composite_for(name, book_id, default_value=default_value) try: field = self.fields[name] except KeyError: return default_value if field.is_multiple: default_value = field.default_value try: return field.for_book(book_id, default_value=default_value) except (KeyError, IndexError): return default_value

[documentos] @read_api def fast_field_for(self, field_obj, book_id, default_value=None): ' Same as field_for, except that it avoids the extra lookup to get the field object ' if field_obj.is_composite: return field_obj.get_value_with_cache(book_id, self._get_proxy_metadata) if field_obj.is_multiple: default_value = field_obj.default_value try: return field_obj.for_book(book_id, default_value=default_value) except (KeyError, IndexError): return default_value

[documentos] @read_api def all_field_for(self, field, book_ids, default_value=None): ' Same as field_for, except that it operates on multiple books at once ' field_obj = self.fields[field] return {book_id:self._fast_field_for(field_obj, book_id, default_value=default_value) for book_id in book_ids}

@read_api def composite_for(self, name, book_id, mi=None, default_value=''): try: f = self.fields[name] except KeyError: return default_value if mi is None: return f.get_value_with_cache(book_id, self._get_proxy_metadata) else: return f._render_composite_with_cache(book_id, mi, mi.formatter, mi.template_cache)

[documentos] @read_api def field_ids_for(self, name, book_id): ''' Return the ids (as a tuple) for the values that the field ``name`` has on the book identified by ``book_id``. If there are no values, or no such book, or no such field, an empty tuple is returned. ''' try: return self.fields[name].ids_for_book(book_id) except (KeyError, IndexError): return ()

[documentos] @read_api def books_for_field(self, name, item_id): ''' Return all the books associated with the item identified by ``item_id``, where the item belongs to the field ``name``. Returned value is a set of book ids, or the empty set if the item or the field does not exist. ''' try: return self.fields[name].books_for(item_id) except (KeyError, IndexError): return set()

[documentos] @read_api def all_book_ids(self, type=frozenset): ''' Frozen set of all known book ids. ''' return type(self.fields['uuid'].table.book_col_map)

[documentos] @read_api def all_field_ids(self, name): ''' Frozen set of ids for all values in the field ``name``. ''' return frozenset(iter(self.fields[name]))

[documentos] @read_api def all_field_names(self, field): ''' Frozen set of all fields names (should only be used for many-one and many-many fields) ''' if field == 'formats': return frozenset(self.fields[field].table.col_book_map) try: return frozenset(self.fields[field].table.id_map.values()) except AttributeError: raise ValueError('%s is not a many-one or many-many field' % field)

[documentos] @read_api def get_usage_count_by_id(self, field): ''' Return a mapping of id to usage count for all values of the specified field, which must be a many-one or many-many field. ''' try: return {k:len(v) for k, v in iteritems(self.fields[field].table.col_book_map)} except AttributeError: raise ValueError('%s is not a many-one or many-many field' % field)

[documentos] @read_api def get_id_map(self, field): ''' Return a mapping of id numbers to values for the specified field. The field must be a many-one or many-many field, otherwise a ValueError is raised. ''' try: return self.fields[field].table.id_map.copy() except AttributeError: if field == 'title': return self.fields[field].table.book_col_map.copy() raise ValueError('%s is not a many-one or many-many field' % field)

[documentos] @read_api def get_item_name(self, field, item_id): ''' Return the item name for the item specified by item_id in the specified field. See also :meth:`get_id_map`.''' return self.fields[field].table.id_map[item_id]

[documentos] @read_api def get_item_id(self, field, item_name): ' Return the item id for item_name (case-insensitive) or None if not found ' q = icu_lower(item_name) try: for item_id, item_val in self.fields[field].table.id_map.items(): if icu_lower(item_val) == q: return item_id except KeyError: return None

[documentos] @read_api def get_item_ids(self, field, item_names): ' Return the item id for item_name (case-insensitive) ' rmap = {icu_lower(v) if isinstance(v, str) else v:k for k, v in iteritems(self.fields[field].table.id_map)} return {name:rmap.get(icu_lower(name) if isinstance(name, str) else name, None) for name in item_names}

[documentos] @read_api def get_item_name_map(self, field, normalize_func=None): ' Return mapping of item values to ids ' if normalize_func is None: return {v:k for k, v in self.fields[field].table.id_map.items()} return {normalize_func(v):k for k, v in self.fields[field].table.id_map.items()}

[documentos] @read_api def author_data(self, author_ids=None): ''' Return author data as a dictionary with keys: name, sort, link If no authors with the specified ids are found an empty dictionary is returned. If author_ids is None, data for all authors is returned. ''' af = self.fields['authors'] if author_ids is None: return {aid:af.author_data(aid) for aid in af.table.id_map} return {aid:af.author_data(aid) for aid in author_ids if aid in af.table.id_map}

[documentos] @read_api def format_hash(self, book_id, fmt): ''' Return the hash of the specified format for the specified book. The kind of hash is backend dependent, but is usually SHA-256. ''' try: name = self.fields['formats'].format_fname(book_id, fmt) path = self._field_for('path', book_id).replace('/', os.sep) except: raise NoSuchFormat('Record %d has no fmt: %s'%(book_id, fmt)) return self.backend.format_hash(book_id, fmt, name, path)

[documentos] @api def format_metadata(self, book_id, fmt, allow_cache=True, update_db=False): ''' Return the path, size and mtime for the specified format for the specified book. You should not use path unless you absolutely have to, since accessing it directly breaks the threadsafe guarantees of this API. Instead use the :meth:`copy_format_to` method. :param allow_cache: If ``True`` cached values are used, otherwise a slow filesystem access is done. The cache values could be out of date if access was performed to the filesystem outside of this API. :param update_db: If ``True`` The max_size field of the database is updated for this book. ''' if not fmt: return {} fmt = fmt.upper() # allow_cache and update_db are mutually exclusive. Give priority to update_db if allow_cache and not update_db: x = self.format_metadata_cache[book_id].get(fmt, None) if x is not None: return x with self.safe_read_lock: try: name = self.fields['formats'].format_fname(book_id, fmt) path = self._field_for('path', book_id).replace('/', os.sep) except: return {} ans = {} if path and name: ans = self.backend.format_metadata(book_id, fmt, name, path) self.format_metadata_cache[book_id][fmt] = ans if update_db and 'size' in ans: with self.write_lock: max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, ans['size'], self.backend) self.fields['size'].table.update_sizes({book_id: max_size}) return ans

@read_api def format_files(self, book_id): field = self.fields['formats'] fmts = field.table.book_col_map.get(book_id, ()) return {fmt:field.format_fname(book_id, fmt) for fmt in fmts} @read_api def format_db_size(self, book_id, fmt): field = self.fields['formats'] return field.format_size(book_id, fmt)

[documentos] @read_api def pref(self, name, default=None, namespace=None): ' Return the value for the specified preference or the value specified as ``default`` if the preference is not set. ' if namespace is not None: return self.backend.prefs.get_namespaced(namespace, name, default) return self.backend.prefs.get(name, default)

[documentos] @write_api def set_pref(self, name, val, namespace=None): ' Set the specified preference to the specified value. See also :meth:`pref`. ' if namespace is not None: self.backend.prefs.set_namespaced(namespace, name, val) return self.backend.prefs.set(name, val) if name in ('grouped_search_terms', 'virtual_libraries'): self._clear_search_caches() if name in dynamic_category_preferences: self._initialize_dynamic_categories()

[documentos] @api def get_metadata(self, book_id, get_cover=False, get_user_categories=True, cover_as_data=False): ''' Return metadata for the book identified by book_id as a :class:`calibre.ebooks.metadata.book.base.Metadata` object. Note that the list of formats is not verified. If get_cover is True, the cover is returned, either a path to temp file as mi.cover or if cover_as_data is True then as mi.cover_data. ''' # Check if virtual_libraries_for_books rebuilt its cache. If it did then # we must clear the composite caches so the new data can be taken into # account. Clearing the caches requires getting a write lock, so it must # be done outside of the closure of _get_metadata(). composite_cache_needs_to_be_cleared = False with self.safe_read_lock: vl_cache_was_none = self.vls_for_books_cache is None mi = self._get_metadata(book_id, get_user_categories=get_user_categories) if vl_cache_was_none and self.vls_for_books_cache is not None: composite_cache_needs_to_be_cleared = True if composite_cache_needs_to_be_cleared: try: self.clear_composite_caches() except LockingError: # We can't clear the composite caches because a read lock is set. # As a consequence the value of a composite column that calls # virtual_libraries() might be wrong. Oh well. Log and keep running. print('Couldn\'t get write lock after vls_for_books_cache was loaded', file=sys.stderr) traceback.print_exc() if get_cover: if cover_as_data: cdata = self.cover(book_id) if cdata: mi.cover_data = ('jpeg', cdata) else: mi.cover = self.cover(book_id, as_path=True) return mi

[documentos] @read_api def get_proxy_metadata(self, book_id): ''' Like :meth:`get_metadata` except that it returns a ProxyMetadata object that only reads values from the database on demand. This is much faster than get_metadata when only a small number of fields need to be accessed from the returned metadata object. ''' return ProxyMetadata(self, book_id)

[documentos] @api def cover(self, book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False): ''' Return the cover image or None. By default, returns the cover as a bytestring. WARNING: Using as_path will copy the cover to a temp file and return the path to the temp file. You should delete the temp file when you are done with it. :param as_file: If True return the image as an open file object (a SpooledTemporaryFile) :param as_image: If True return the image as a QImage object :param as_pixmap: If True return the image as a QPixmap object :param as_path: If True return the image as a path pointing to a temporary file ''' if as_file: ret = SpooledTemporaryFile(SPOOL_SIZE) if not self.copy_cover_to(book_id, ret): ret.close() return ret.seek(0) elif as_path: pt = PersistentTemporaryFile('_dbcover.jpg') with pt: if not self.copy_cover_to(book_id, pt): return ret = pt.name elif as_pixmap or as_image: from qt.core import QImage, QPixmap ret = QImage() if as_image else QPixmap() with self.safe_read_lock: path = self._format_abspath(book_id, '__COVER_INTERNAL__') if path: ret.load(path) else: buf = BytesIO() if not self.copy_cover_to(book_id, buf): return ret = buf.getvalue() return ret

@read_api def cover_or_cache(self, book_id, timestamp, as_what='bytes'): try: path = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: return False, None, None return self.backend.cover_or_cache(path, timestamp, as_what) @read_api def cover_last_modified(self, book_id): try: path = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: return return self.backend.cover_last_modified(path)

[documentos] @read_api def copy_cover_to(self, book_id, dest, use_hardlink=False, report_file_size=None): ''' Copy the cover to the file like object ``dest``. Returns False if no cover exists or dest is the same file as the current cover. dest can also be a path in which case the cover is copied to it if and only if the path is different from the current path (taking case sensitivity into account). ''' try: path = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: return False return self.backend.copy_cover_to(path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[documentos] @write_api def compress_covers(self, book_ids, jpeg_quality=100, progress_callback=None): ''' Compress the cover images for the specified books. A compression quality of 100 will perform lossless compression, otherwise lossy compression. The progress callback will be called with the book_id and the old and new sizes for each book that has been processed. If an error occurs, the new size will be a string with the error details. ''' jpeg_quality = max(10, min(jpeg_quality, 100)) path_map = {} for book_id in book_ids: try: path_map[book_id] = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: continue self.backend.compress_covers(path_map, jpeg_quality, progress_callback)

[documentos] @read_api def copy_format_to(self, book_id, fmt, dest, use_hardlink=False, report_file_size=None): ''' Copy the format ``fmt`` to the file like object ``dest``. If the specified format does not exist, raises :class:`NoSuchFormat` error. dest can also be a path (to a file), in which case the format is copied to it, iff the path is different from the current path (taking case sensitivity into account). ''' fmt = (fmt or '').upper() try: name = self.fields['formats'].format_fname(book_id, fmt) path = self._field_for('path', book_id).replace('/', os.sep) except (KeyError, AttributeError): raise NoSuchFormat('Record %d has no %s file'%(book_id, fmt)) return self.backend.copy_format_to(book_id, fmt, name, path, dest, use_hardlink=use_hardlink, report_file_size=report_file_size)

[documentos] @read_api def format_abspath(self, book_id, fmt): ''' Return absolute path to the e-book file of format `format`. You should almost never use this, as it breaks the threadsafe promise of this API. Instead use, :meth:`copy_format_to`. Currently used only in calibredb list, the viewer, edit book, compare_format to original format, open with, bulk metadata edit and the catalogs (via get_data_as_dict()). Apart from the viewer, open with and edit book, I don't believe any of the others do any file write I/O with the results of this call. ''' fmt = (fmt or '').upper() try: path = self._field_for('path', book_id).replace('/', os.sep) except: return None if path: if fmt == '__COVER_INTERNAL__': return self.backend.cover_abspath(book_id, path) else: try: name = self.fields['formats'].format_fname(book_id, fmt) except: return None if name: return self.backend.format_abspath(book_id, fmt, name, path)

[documentos] @read_api def has_format(self, book_id, fmt): 'Return True iff the format exists on disk' fmt = (fmt or '').upper() try: name = self.fields['formats'].format_fname(book_id, fmt) path = self._field_for('path', book_id).replace('/', os.sep) except: return False return self.backend.has_format(book_id, fmt, name, path)

[documentos] @api def save_original_format(self, book_id, fmt): ' Save a copy of the specified format as ORIGINAL_FORMAT, overwriting any existing ORIGINAL_FORMAT. ' fmt = fmt.upper() if 'ORIGINAL' in fmt: raise ValueError('Cannot save original of an original fmt') fmtfile = self.format(book_id, fmt, as_file=True) if fmtfile is None: return False with fmtfile: nfmt = 'ORIGINAL_'+fmt return self.add_format(book_id, nfmt, fmtfile, run_hooks=False)

[documentos] @write_api def restore_original_format(self, book_id, original_fmt): ''' Restore the specified format from the previously saved ORIGINAL_FORMAT, if any. Return True on success. The ORIGINAL_FORMAT is deleted after a successful restore. ''' original_fmt = original_fmt.upper() fmt = original_fmt.partition('_')[2] try: ofmt_name = self.fields['formats'].format_fname(book_id, original_fmt) path = self._field_for('path', book_id).replace('/', os.sep) except Exception: return False if self.backend.is_format_accessible(book_id, original_fmt, ofmt_name, path): self.add_format(book_id, fmt, BytesIO(), run_hooks=False) fmt_name = self.fields['formats'].format_fname(book_id, fmt) file_size = self.backend.rename_format_file(book_id, ofmt_name, original_fmt, fmt_name, fmt, path) self.fields['formats'].table.update_fmt(book_id, fmt, fmt_name, file_size, self.backend) self._remove_formats({book_id:(original_fmt,)}) return True return False

[documentos] @read_api def formats(self, book_id, verify_formats=True): ''' Return tuple of all formats for the specified book. If verify_formats is True, verifies that the files exist on disk. ''' ans = self.field_for('formats', book_id) if verify_formats and ans: try: path = self._field_for('path', book_id).replace('/', os.sep) except: return () def verify(fmt): try: name = self.fields['formats'].format_fname(book_id, fmt) except: return False return self.backend.has_format(book_id, fmt, name, path) ans = tuple(x for x in ans if verify(x)) return ans

[documentos] @api def format(self, book_id, fmt, as_file=False, as_path=False, preserve_filename=False): ''' Return the e-book format as a bytestring or `None` if the format doesn't exist, or we don't have permission to write to the e-book file. :param as_file: If True the e-book format is returned as a file object. Note that the file object is a SpooledTemporaryFile, so if what you want to do is copy the format to another file, use :meth:`copy_format_to` instead for performance. :param as_path: Copies the format file to a temp file and returns the path to the temp file :param preserve_filename: If True and returning a path the filename is the same as that used in the library. Note that using this means that repeated calls yield the same temp file (which is re-created each time) ''' fmt = (fmt or '').upper() ext = ('.'+fmt.lower()) if fmt else '' if as_path: if preserve_filename: with self.safe_read_lock: try: fname = self.fields['formats'].format_fname(book_id, fmt) except: return None fname += ext bd = base_dir() d = os.path.join(bd, 'format_abspath') try: os.makedirs(d) except: pass ret = os.path.join(d, fname) try: self.copy_format_to(book_id, fmt, ret) except NoSuchFormat: return None else: with PersistentTemporaryFile(ext) as pt: try: self.copy_format_to(book_id, fmt, pt) except NoSuchFormat: return None ret = pt.name elif as_file: with self.safe_read_lock: try: fname = self.fields['formats'].format_fname(book_id, fmt) except: return None fname += ext ret = SpooledTemporaryFile(SPOOL_SIZE) try: self.copy_format_to(book_id, fmt, ret) except NoSuchFormat: ret.close() return None ret.seek(0) # Various bits of code try to use the name as the default # title when reading metadata, so set it ret.name = fname else: buf = BytesIO() try: self.copy_format_to(book_id, fmt, buf) except NoSuchFormat: return None ret = buf.getvalue() return ret

@read_api def newly_added_book_ids(self, count=5, book_ids=None) -> list[int]: ids_to_sort = self._all_book_ids(list) if book_ids is None else list(book_ids) ids_to_sort.sort(reverse=True) return ids_to_sort[:count] @read_api def size_stats(self) -> dict[str, int]: return self.backend.size_stats()

[documentos] @read_api def multisort(self, fields, ids_to_sort=None, virtual_fields=None): ''' Return a list of sorted book ids. If ids_to_sort is None, all book ids are returned. fields must be a list of 2-tuples of the form (field_name, ascending=True or False). The most significant field is the first 2-tuple. ''' ids_to_sort = self._all_book_ids() if ids_to_sort is None else ids_to_sort get_metadata = self._get_proxy_metadata lang_map = self.fields['languages'].book_value_map virtual_fields = virtual_fields or {} fm = {'title':'sort', 'authors':'author_sort'} def sort_key_func(field): 'Handle series type fields, virtual fields and the id field' idx = field + '_index' is_series = idx in self.fields try: func = self.fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map) except KeyError: if field == 'id': return IDENTITY else: return virtual_fields[fm.get(field, field)].sort_keys_for_books(get_metadata, lang_map) if is_series: idx_func = self.fields[idx].sort_keys_for_books(get_metadata, lang_map) def skf(book_id): return (func(book_id), idx_func(book_id)) return skf return func # Sort only once on any given field fields = uniq(fields, operator.itemgetter(0)) if len(fields) == 1: keyfunc = sort_key_func(fields[0][0]) reverse = not fields[0][1] try: return sorted(ids_to_sort, key=keyfunc, reverse=reverse) except Exception as err: print('Failed to sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr) try: return sorted(ids_to_sort, key=type_safe_sort_key_function(keyfunc), reverse=reverse) except Exception as err: print('Failed to type-safe sort database on field:', fields[0][0], 'with error:', err, file=sys.stderr) return sorted(ids_to_sort, reverse=reverse) sort_key_funcs = tuple(sort_key_func(field) for field, order in fields) orders = tuple(1 if order else -1 for _, order in fields) Lazy = object() # Lazy load the sort keys for sub-sort fields class SortKey: __slots__ = 'book_id', 'sort_key' def __init__(self, book_id): self.book_id = book_id # Calculate only the first sub-sort key since that will always be used self.sort_key = [key(book_id) if i == 0 else Lazy for i, key in enumerate(sort_key_funcs)] def compare_to_other(self, other): for i, (order, self_key, other_key) in enumerate(zip(orders, self.sort_key, other.sort_key)): if self_key is Lazy: self_key = self.sort_key[i] = sort_key_funcs[i](self.book_id) if other_key is Lazy: other_key = other.sort_key[i] = sort_key_funcs[i](other.book_id) ans = cmp(self_key, other_key) if ans != 0: return ans * order return 0 def __eq__(self, other): return self.compare_to_other(other) == 0 def __ne__(self, other): return self.compare_to_other(other) != 0 def __lt__(self, other): return self.compare_to_other(other) < 0 def __le__(self, other): return self.compare_to_other(other) <= 0 def __gt__(self, other): return self.compare_to_other(other) > 0 def __ge__(self, other): return self.compare_to_other(other) >= 0 return sorted(ids_to_sort, key=SortKey)

[documentos] @read_api def search(self, query, restriction='', virtual_fields=None, book_ids=None): ''' Search the database for the specified query, returning a set of matched book ids. :param restriction: A restriction that is ANDed to the specified query. Note that restrictions are cached, therefore the search for a AND b will be slower than a with restriction b. :param virtual_fields: Used internally (virtual fields such as on_device to search over). :param book_ids: If not None, a set of book ids for which books will be searched instead of searching all books. ''' return self._search_api(self, query, restriction, virtual_fields=virtual_fields, book_ids=book_ids)

[documentos] @read_api def books_in_virtual_library(self, vl, search_restriction=None, virtual_fields=None): ' Return the set of books in the specified virtual library ' vl = self._pref('virtual_libraries', {}).get(vl) if vl else None if not vl and not search_restriction: return self.all_book_ids() # We utilize the search restriction cache to speed this up srch = partial(self._search, virtual_fields=virtual_fields) if vl: if search_restriction: return frozenset(srch('', vl) & srch('', search_restriction)) return frozenset(srch('', vl)) return frozenset(srch('', search_restriction))

@read_api def number_of_books_in_virtual_library(self, vl=None, search_restriction=None): if not vl and not search_restriction: return len(self.fields['uuid'].table.book_col_map) return len(self.books_in_virtual_library(vl, search_restriction))

[documentos] @api def get_categories(self, sort='name', book_ids=None, already_fixed=None, first_letter_sort=False): ' Used internally to implement the Tag Browser ' try: with self.safe_read_lock: return get_categories(self, sort=sort, book_ids=book_ids, first_letter_sort=first_letter_sort) except InvalidLinkTable as err: bad_field = err.field_name if bad_field == already_fixed: raise with self.write_lock: self.fields[bad_field].table.fix_link_table(self.backend) return self.get_categories(sort=sort, book_ids=book_ids, already_fixed=bad_field)

@write_api def update_last_modified(self, book_ids, now=None): if book_ids: if now is None: now = nowf() f = self.fields['last_modified'] f.writer.set_books({book_id:now for book_id in book_ids}, self.backend) if self.composites: self._clear_composite_caches(book_ids) self._clear_search_caches(book_ids) @write_api def mark_as_dirty(self, book_ids): self._update_last_modified(book_ids) already_dirtied = set(self.dirtied_cache).intersection(book_ids) new_dirtied = book_ids - already_dirtied already_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(already_dirtied)} if already_dirtied: self.dirtied_sequence = max(itervalues(already_dirtied)) + 1 self.dirtied_cache.update(already_dirtied) if new_dirtied: self.backend.dirty_books(new_dirtied) new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)} self.dirtied_sequence = max(itervalues(new_dirtied)) + 1 self.dirtied_cache.update(new_dirtied) @write_api def commit_dirty_cache(self): if self.dirtied_cache: self.backend.dirty_books(self.dirtied_cache) @write_api def check_dirtied_annotations(self): if not self.backend.dirty_books_with_dirtied_annotations(): return book_ids = set(self.backend.dirtied_books()) new_dirtied = book_ids - set(self.dirtied_cache) if new_dirtied: new_dirtied = {book_id:self.dirtied_sequence+i for i, book_id in enumerate(new_dirtied)} self.dirtied_sequence = max(itervalues(new_dirtied)) + 1 self.dirtied_cache.update(new_dirtied)

[documentos] @write_api def set_field(self, name, book_id_to_val_map, allow_case_change=True, do_path_update=True): ''' Set the values of the field specified by ``name``. Returns the set of all book ids that were affected by the change. :param book_id_to_val_map: Mapping of book_ids to values that should be applied. :param allow_case_change: If True, the case of many-one or many-many fields will be changed. For example, if a book has the tag ``tag1`` and you set the tag for another book to ``Tag1`` then the both books will have the tag ``Tag1`` if allow_case_change is True, otherwise they will both have the tag ``tag1``. :param do_path_update: Used internally, you should never change it. ''' f = self.fields[name] is_series = f.metadata['datatype'] == 'series' update_path = name in {'title', 'authors'} if update_path and iswindows: paths = (x for x in (self._field_for('path', book_id) for book_id in book_id_to_val_map) if x) self.backend.windows_check_if_files_in_use(paths) if is_series: bimap, simap = {}, {} sfield = self.fields[name + '_index'] for k, v in iteritems(book_id_to_val_map): if isinstance(v, string_or_bytes): v, sid = get_series_values(v) else: v = sid = None if sid is None and name.startswith('#'): sid = self._fast_field_for(sfield, k) sid = 1.0 if sid is None else sid # The value to be set the db link table bimap[k] = v if sid is not None: simap[k] = sid book_id_to_val_map = bimap dirtied = f.writer.set_books( book_id_to_val_map, self.backend, allow_case_change=allow_case_change) if is_series and simap: sf = self.fields[f.name+'_index'] dirtied |= sf.writer.set_books(simap, self.backend, allow_case_change=False) if dirtied: if update_path and do_path_update: self._update_path(dirtied, mark_as_dirtied=False) self._mark_as_dirty(dirtied) self._clear_link_map_cache(dirtied) self.event_dispatcher(EventType.metadata_changed, name, dirtied) return dirtied

@write_api def update_path(self, book_ids, mark_as_dirtied=True): for book_id in book_ids: title = self._field_for('title', book_id, default_value=_('Unknown')) try: author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0] except IndexError: author = _('Unknown') self.backend.update_path(book_id, title, author, self.fields['path'], self.fields['formats']) self.format_metadata_cache.pop(book_id, None) if mark_as_dirtied: self._mark_as_dirty(book_ids) self._clear_link_map_cache(book_ids) @read_api def get_a_dirtied_book(self): if self.dirtied_cache: return random.choice(tuple(self.dirtied_cache)) return None def _metadata_as_object_for_dump(self, book_id): mi = self._get_metadata(book_id) # Always set cover to cover.jpg. Even if cover doesn't exist, # no harm done. This way no need to call dirtied when # cover is set/removed mi.cover = 'cover.jpg' mi.all_annotations = self._all_annotations_for_book(book_id) return mi @read_api def get_metadata_for_dump(self, book_id): mi = None # get the current sequence number for this book to pass back to the # backup thread. This will avoid double calls in the case where the # thread has not done the work between the put and the get_metadata sequence = self.dirtied_cache.get(book_id, None) if sequence is not None: try: # While a book is being created, the path is empty. Don't bother to # try to write the opf, because it will go to the wrong folder. if self._field_for('path', book_id): mi = self._metadata_as_object_for_dump(book_id) except: # This almost certainly means that the book has been deleted while # the backup operation sat in the queue. traceback.print_exc() return mi, sequence @write_api def clear_dirtied(self, book_id, sequence): # Clear the dirtied indicator for the books. This is used when fetching # metadata, creating an OPF, and writing a file are separated into steps. # The last step is clearing the indicator dc_sequence = self.dirtied_cache.get(book_id, None) if dc_sequence is None or sequence is None or dc_sequence == sequence: self.backend.mark_book_as_clean(book_id) self.dirtied_cache.pop(book_id, None) @write_api def write_backup(self, book_id, raw): try: path = self._field_for('path', book_id).replace('/', os.sep) except: return self.backend.write_backup(path, raw) @read_api def dirty_queue_length(self): return len(self.dirtied_cache)

[documentos] @read_api def read_backup(self, book_id): ''' Return the OPF metadata backup for the book as a bytestring or None if no such backup exists. ''' try: path = self._field_for('path', book_id).replace('/', os.sep) except: return try: return self.backend.read_backup(path) except OSError: return None

@write_api def dump_metadata(self, book_ids=None, remove_from_dirtied=True, callback=None): # Write metadata for each record to an individual OPF file. If callback # is not None, it is called once at the start with the number of book_ids # being processed. And once for every book_id, with arguments (book_id, # mi, ok). if book_ids is None: book_ids = set(self.dirtied_cache) if callback is not None: callback(len(book_ids), True, False) for book_id in book_ids: if self._field_for('path', book_id) is None: if callback is not None: callback(book_id, None, False) continue mi, sequence = self._get_metadata_for_dump(book_id) if mi is None: if callback is not None: callback(book_id, mi, False) continue try: raw = metadata_to_opf(mi) self._write_backup(book_id, raw) if remove_from_dirtied: self._clear_dirtied(book_id, sequence) except: pass if callback is not None: callback(book_id, mi, True)

[documentos] @write_api def set_cover(self, book_id_data_map): ''' Set the cover for this book. The data can be either a QImage, QPixmap, file object or bytestring. It can also be None, in which case any existing cover is removed. ''' for book_id, data in iteritems(book_id_data_map): try: path = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: self._update_path((book_id,)) path = self._field_for('path', book_id).replace('/', os.sep) self.backend.set_cover(book_id, path, data) for cc in self.cover_caches: cc.invalidate(book_id_data_map) return self._set_field('cover', { book_id:(0 if data is None else 1) for book_id, data in iteritems(book_id_data_map)})

@write_api def add_cover_cache(self, cover_cache): if not callable(cover_cache.invalidate): raise ValueError('Cover caches must have an invalidate method') self.cover_caches.add(cover_cache) @write_api def remove_cover_cache(self, cover_cache): self.cover_caches.discard(cover_cache)

[documentos] @write_api def set_metadata(self, book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False): ''' Set metadata for the book `id` from the `Metadata` object `mi` Setting force_changes=True will force set_metadata to update fields even if mi contains empty values. In this case, 'None' is distinguished from 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is. The tags, identifiers, and cover attributes are special cases. Tags and identifiers cannot be set to None so they will always be replaced if force_changes is true. You must ensure that mi contains the values you want the book to have. Covers are always changed if a new cover is provided, but are never deleted. Also note that force_changes has no effect on setting title or authors. ''' dirtied = set() try: # Handle code passing in an OPF object instead of a Metadata object mi = mi.to_book_metadata() except (AttributeError, TypeError): pass def set_field(name, val): dirtied.update(self._set_field(name, {book_id:val}, do_path_update=False, allow_case_change=allow_case_change)) path_changed = False if set_title and mi.title: path_changed = True set_field('title', mi.title) authors_changed = False if set_authors: path_changed = True if not mi.authors: mi.authors = [_('Unknown')] authors = [] for a in mi.authors: authors += string_to_authors(a) set_field('authors', authors) authors_changed = True if path_changed: self._update_path({book_id}) def protected_set_field(name, val): try: set_field(name, val) except: if ignore_errors: traceback.print_exc() else: raise # force_changes has no effect on cover manipulation try: cdata = mi.cover_data[1] if cdata is None and isinstance(mi.cover, string_or_bytes) and mi.cover and os.access(mi.cover, os.R_OK): with open(mi.cover, 'rb') as f: cdata = f.read() or None if cdata is not None: self._set_cover({book_id: cdata}) except: if ignore_errors: traceback.print_exc() else: raise try: with self.backend.conn: # Speed up set_metadata by not operating in autocommit mode for field in ('rating', 'series_index', 'timestamp'): val = getattr(mi, field) if val is not None: protected_set_field(field, val) val = mi.get('author_sort', None) if authors_changed and (not val or mi.is_null('author_sort')): val = self._author_sort_from_authors(mi.authors) if authors_changed or (force_changes and val is not None) or not mi.is_null('author_sort'): protected_set_field('author_sort', val) for field in ('publisher', 'series', 'tags', 'comments', 'languages', 'pubdate'): val = mi.get(field, None) if (force_changes and val is not None) or not mi.is_null(field): protected_set_field(field, val) val = mi.get('title_sort', None) if (force_changes and val is not None) or not mi.is_null('title_sort'): protected_set_field('sort', val) # identifiers will always be replaced if force_changes is True mi_idents = mi.get_identifiers() if force_changes: protected_set_field('identifiers', mi_idents) elif mi_idents: identifiers = self._field_for('identifiers', book_id, default_value={}) for key, val in iteritems(mi_idents): if val and val.strip(): # Don't delete an existing identifier identifiers[icu_lower(key)] = val protected_set_field('identifiers', identifiers) user_mi = mi.get_all_user_metadata(make_copy=False) fm = self.field_metadata for key in user_mi: if (key in fm and user_mi[key]['datatype'] == fm[key]['datatype'] and ( user_mi[key]['datatype'] != 'text' or ( user_mi[key]['is_multiple'] == fm[key]['is_multiple']))): val = mi.get(key, None) if force_changes or val is not None: protected_set_field(key, val) idx = key + '_index' if idx in self.fields: extra = mi.get_extra(key) if extra is not None or force_changes: protected_set_field(idx, extra) except: # sqlite will rollback the entire transaction, thanks to the with # statement, so we have to re-read everything form the db to ensure # the db and Cache are in sync self._reload_from_db() raise return dirtied

def _do_add_format(self, book_id, fmt, stream, name=None, mtime=None): path = self._field_for('path', book_id) if path is None: # Theoretically, this should never happen, but apparently it # does: https://www.mobileread.com/forums/showthread.php?t=233353 self._update_path({book_id}, mark_as_dirtied=False) path = self._field_for('path', book_id) path = path.replace('/', os.sep) title = self._field_for('title', book_id, default_value=_('Unknown')) try: author = self._field_for('authors', book_id, default_value=(_('Unknown'),))[0] except IndexError: author = _('Unknown') size, fname = self.backend.add_format(book_id, fmt, stream, title, author, path, name, mtime=mtime) return size, fname

[documentos] @api def add_format(self, book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None): ''' Add a format to the specified book. Return True if the format was added successfully. :param replace: If True replace existing format, otherwise if the format already exists, return False. :param run_hooks: If True, file type plugins are run on the format before and after being added. :param dbapi: Internal use only. ''' needs_close = False if run_hooks: # Run import plugins, the write lock is not held to cater for # broken plugins that might spin the event loop by popping up a # message in the GUI during the processing. npath = run_import_plugins(stream_or_path, fmt) fmt = os.path.splitext(npath)[-1].lower().replace('.', '').upper() stream_or_path = open(make_long_path_useable(npath), 'rb') needs_close = True fmt = check_ebook_format(stream_or_path, fmt) with self.write_lock: if not self._has_id(book_id): raise NoSuchBook(book_id) fmt = (fmt or '').upper() self.format_metadata_cache[book_id].pop(fmt, None) try: name = self.fields['formats'].format_fname(book_id, fmt) except Exception: name = None if name and not replace: if needs_close: stream_or_path.close() return False if hasattr(stream_or_path, 'read'): stream = stream_or_path else: stream = open(make_long_path_useable(stream_or_path), 'rb') needs_close = True try: size, fname = self._do_add_format(book_id, fmt, stream, name) finally: if needs_close: stream.close() del stream max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend) self.fields['size'].table.update_sizes({book_id: max_size}) self._update_last_modified((book_id,)) self.event_dispatcher(EventType.format_added, book_id, fmt) if run_hooks: # Run post import plugins, the write lock is released so the plugin # can call api without a locking violation. run_plugins_on_postimport(dbapi or self, book_id, fmt) stream_or_path.close() self.queue_next_fts_job() return True

[documentos] @write_api def remove_formats(self, formats_map, db_only=False): ''' Remove the specified formats from the specified books. :param formats_map: A mapping of book_id to a list of formats to be removed from the book. :param db_only: If True, only remove the record for the format from the db, do not delete the actual format file from the filesystem. :return: A map of book id to set of formats actually deleted from the filesystem for that book ''' table = self.fields['formats'].table formats_map = {book_id:frozenset((f or '').upper() for f in fmts) for book_id, fmts in iteritems(formats_map)} removed_map = {} for book_id, fmts in iteritems(formats_map): for fmt in fmts: self.format_metadata_cache[book_id].pop(fmt, None) if not db_only: removes = defaultdict(set) metadata_map = {} for book_id, fmts in iteritems(formats_map): try: path = self._field_for('path', book_id).replace('/', os.sep) except: continue for fmt in fmts: try: name = self.fields['formats'].format_fname(book_id, fmt) except: continue if name and path: removes[book_id].add((fmt, name, path)) if removes[book_id]: metadata_map[book_id] = {'title': self._field_for('title', book_id), 'authors': self._field_for('authors', book_id)} if removes: removed_map = self.backend.remove_formats(removes, metadata_map) size_map = table.remove_formats(formats_map, self.backend) self.fields['size'].table.update_sizes(size_map) for book_id, fmts in iteritems(formats_map): for fmt in fmts: run_plugins_on_postdelete(self, book_id, fmt) self._update_last_modified(tuple(formats_map)) self.event_dispatcher(EventType.formats_removed, formats_map) return removed_map

[documentos] @read_api def get_next_series_num_for(self, series, field='series', current_indices=False): ''' Return the next series index for the specified series, taking into account the various preferences that control next series number generation. :param field: The series-like field (defaults to the builtin series column) :param current_indices: If True, returns a mapping of book_id to current series_index value instead. ''' books = () sf = self.fields[field] if series: q = icu_lower(series) for val, book_ids in sf.iter_searchable_values(self._get_proxy_metadata, frozenset(self._all_book_ids())): if q == icu_lower(val): books = book_ids break idf = sf.index_field index_map = {book_id:self._fast_field_for(idf, book_id, default_value=1.0) for book_id in books} if current_indices: return index_map series_indices = sorted(index_map.values(), key=lambda s: s or 0) return _get_next_series_num_for_list(tuple(series_indices), unwrap=False)

[documentos] @read_api def author_sort_from_authors(self, authors, key_func=icu_lower): '''Given a list of authors, return the author_sort string for the authors, preferring the author sort associated with the author over the computed string. ''' table = self.fields['authors'].table result = [] rmap = {key_func(v):k for k, v in iteritems(table.id_map)} for aut in authors: aid = rmap.get(key_func(aut), None) result.append(author_to_author_sort(aut) if aid is None else table.asort_map[aid]) return ' & '.join(_f for _f in result if _f)

[documentos] @read_api def data_for_has_book(self): ''' Return data suitable for use in :meth:`has_book`. This can be used for an implementation of :meth:`has_book` in a worker process without access to the db. ''' try: return {icu_lower(title) for title in itervalues(self.fields['title'].table.book_col_map)} except TypeError: # Some non-unicode titles in the db return {icu_lower(as_unicode(title)) for title in itervalues(self.fields['title'].table.book_col_map)}

[documentos] @read_api def has_book(self, mi): ''' Return True iff the database contains an entry with the same title as the passed in Metadata object. The comparison is case-insensitive. See also :meth:`data_for_has_book`. ''' title = mi.title if title: if isbytestring(title): title = title.decode(preferred_encoding, 'replace') q = icu_lower(title).strip() for title in itervalues(self.fields['title'].table.book_col_map): if q == icu_lower(title): return True return False

[documentos] @read_api def has_id(self, book_id): ' Return True iff the specified book_id exists in the db ''' return book_id in self.fields['title'].table.book_col_map

@write_api def create_book_entry(self, mi, cover=None, add_duplicates=True, force_id=None, apply_import_tags=True, preserve_uuid=False): if mi.tags: mi.tags = list(mi.tags) if apply_import_tags: _add_newbook_tag(mi) _add_default_custom_column_values(mi, self.field_metadata) if not add_duplicates and self._has_book(mi): return series_index = (self._get_next_series_num_for(mi.series) if mi.series_index is None else mi.series_index) try: series_index = float(series_index) except Exception: try: series_index = float(self._get_next_series_num_for(mi.series)) except Exception: series_index = 1.0 if not mi.authors: mi.authors = (_('Unknown'),) aus = mi.author_sort if not mi.is_null('author_sort') else self._author_sort_from_authors(mi.authors) mi.title = mi.title or _('Unknown') if isbytestring(aus): aus = aus.decode(preferred_encoding, 'replace') if isbytestring(mi.title): mi.title = mi.title.decode(preferred_encoding, 'replace') if force_id is None: self.backend.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)', (mi.title, series_index, aus)) else: self.backend.execute('INSERT INTO books(id, title, series_index, author_sort) VALUES (?, ?, ?, ?)', (force_id, mi.title, series_index, aus)) book_id = self.backend.last_insert_rowid() self.event_dispatcher(EventType.book_created, book_id) mi.timestamp = utcnow() if mi.timestamp is None else mi.timestamp mi.pubdate = UNDEFINED_DATE if mi.pubdate is None else mi.pubdate if cover is not None: mi.cover, mi.cover_data = None, (None, cover) self._set_metadata(book_id, mi, ignore_errors=True) lm = getattr(mi, 'link_maps', None) if lm: for field, link_map in lm.items(): if self._has_link_map(field): self._set_link_map(field, link_map, only_set_if_no_existing_link=True) if preserve_uuid and mi.uuid: self._set_field('uuid', {book_id:mi.uuid}) # Update the caches for fields from the books table self.fields['size'].table.book_col_map[book_id] = 0 row = next(self.backend.execute('SELECT sort, series_index, author_sort, uuid, has_cover FROM books WHERE id=?', (book_id,))) for field, val in zip(('sort', 'series_index', 'author_sort', 'uuid', 'cover'), row): if field == 'cover': val = bool(val) elif field == 'uuid': self.fields[field].table.uuid_to_id_map[val] = book_id self.fields[field].table.book_col_map[book_id] = val return book_id

[documentos] @api def add_books(self, books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None): ''' Add the specified books to the library. Books should be an iterable of 2-tuples, each 2-tuple of the form :code:`(mi, format_map)` where mi is a Metadata object and format_map is a dictionary of the form :code:`{fmt: path_or_stream}`, for example: :code:`{'EPUB': '/path/to/file.epub'}`. Returns a pair of lists: :code:`ids, duplicates`. ``ids`` contains the book ids for all newly created books in the database. ``duplicates`` contains the :code:`(mi, format_map)` for all books that already exist in the database as per the simple duplicate detection heuristic used by :meth:`has_book`. ''' duplicates, ids = [], [] for mi, format_map in books: book_id = self.create_book_entry(mi, add_duplicates=add_duplicates, apply_import_tags=apply_import_tags, preserve_uuid=preserve_uuid) if book_id is None: duplicates.append((mi, format_map)) else: fmt_map = {} ids.append(book_id) for fmt, stream_or_path in format_map.items(): if self.add_format(book_id, fmt, stream_or_path, dbapi=dbapi, run_hooks=run_hooks): fmt_map[fmt.lower()] = getattr(stream_or_path, 'name', stream_or_path) or '<stream>' run_plugins_on_postadd(dbapi or self, book_id, fmt_map) return ids, duplicates

[documentos] @write_api def remove_books(self, book_ids, permanent=False): ''' Remove the books specified by the book_ids from the database and delete their format files. If ``permanent`` is False, then the format files are placed in the per-library trash directory. ''' path_map = {} for book_id in book_ids: try: path = self._field_for('path', book_id).replace('/', os.sep) except Exception: path = None path_map[book_id] = path if not permanent and path: # ensure metadata.opf is written and up-to-date so we can restore the book try: mi = self._metadata_as_object_for_dump(book_id) raw = metadata_to_opf(mi) self.backend.write_backup(path, raw) except Exception: traceback.print_exc() self.backend.remove_books(path_map, permanent=permanent) for field in itervalues(self.fields): try: table = field.table except AttributeError: continue # Some fields like ondevice do not have tables else: table.remove_books(book_ids, self.backend) self._search_api.discard_books(book_ids) self._clear_caches(book_ids=book_ids, template_cache=False, search_cache=False) for cc in self.cover_caches: cc.invalidate(book_ids) self.event_dispatcher(EventType.books_removed, book_ids)

@read_api def author_sort_strings_for_books(self, book_ids): val_map = {} for book_id in book_ids: authors = self._field_ids_for('authors', book_id) adata = self._author_data(authors) val_map[book_id] = tuple(adata[aid]['sort'] for aid in authors) return val_map

[documentos] @write_api def rename_items(self, field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None): ''' Rename items from a many-one or many-many field such as tags or series. :param change_index: When renaming in a series-like field also change the series_index values. :param restrict_to_book_ids: An optional set of book ids for which the rename is to be performed, defaults to all books. ''' f = self.fields[field] affected_books = set() try: sv = f.metadata['is_multiple']['ui_to_list'] except (TypeError, KeyError, AttributeError): sv = None if restrict_to_book_ids is not None: # We have a VL. Only change the item name for those books if not isinstance(restrict_to_book_ids, (Set, MutableSet)): restrict_to_book_ids = frozenset(restrict_to_book_ids) id_map = {} default_process_map = {} for old_id, new_name in iteritems(item_id_to_new_name_map): new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,) # Get a list of books in the VL with the item books_with_id = f.books_for(old_id) books_to_process = books_with_id & restrict_to_book_ids if len(books_with_id) == len(books_to_process): # All the books with the ID are in the VL, so we can use # the normal processing default_process_map[old_id] = new_name elif books_to_process: affected_books.update(books_to_process) newvals = {} for book_id in books_to_process: # Get the current values, remove the one being renamed, then add # the new value(s) back. vals = self._field_for(field, book_id) # Check for is_multiple if isinstance(vals, tuple): # We must preserve order. vals = list(vals) # Don't need to worry about case here because we # are fetching its one-true spelling. But lets be # careful anyway try: dex = vals.index(self._get_item_name(field, old_id)) # This can put the name back with a different case vals[dex] = new_names[0] # now add any other items if they aren't already there if len(new_names) > 1: set_vals = {icu_lower(x) for x in vals} for v in new_names[1:]: lv = icu_lower(v) if lv not in set_vals: vals.append(v) set_vals.add(lv) newvals[book_id] = vals except Exception: traceback.print_exc() else: newvals[book_id] = new_names[0] # Allow case changes self._set_field(field, newvals) id_map[old_id] = self._get_item_id(field, new_names[0]) if default_process_map: ab, idm = self._rename_items(field, default_process_map, change_index=change_index) affected_books.update(ab) id_map.update(idm) self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map) return affected_books, id_map try: func = f.table.rename_item except AttributeError: raise ValueError('Cannot rename items for one-one fields: %s' % field) moved_books = set() id_map = {} for item_id, new_name in iteritems(item_id_to_new_name_map): new_names = tuple(x.strip() for x in new_name.split(sv)) if sv else (new_name,) books, new_id = func(item_id, new_names[0], self.backend) affected_books.update(books) id_map[item_id] = new_id if new_id != item_id: moved_books.update(books) if len(new_names) > 1: # Add the extra items to the books extra = new_names[1:] self._set_field(field, {book_id:self._fast_field_for(f, book_id) + extra for book_id in books}) if affected_books: if field == 'authors': self._set_field('author_sort', {k:' & '.join(v) for k, v in iteritems(self._author_sort_strings_for_books(affected_books))}) self._update_path(affected_books, mark_as_dirtied=False) elif change_index and hasattr(f, 'index_field') and tweaks['series_index_auto_increment'] != 'no_change': for book_id in moved_books: self._set_field(f.index_field.name, {book_id:self._get_next_series_num_for(self._fast_field_for(f, book_id), field=field)}) self._mark_as_dirty(affected_books) self._clear_link_map_cache(affected_books) self.event_dispatcher(EventType.items_renamed, field, affected_books, id_map) return affected_books, id_map

[documentos] @write_api def remove_items(self, field, item_ids, restrict_to_book_ids=None): ''' Delete all items in the specified field with the specified ids. Returns the set of affected book ids. ``restrict_to_book_ids`` is an optional set of books ids. If specified the items will only be removed from those books. ''' field = self.fields[field] if restrict_to_book_ids is not None and not isinstance(restrict_to_book_ids, (MutableSet, Set)): restrict_to_book_ids = frozenset(restrict_to_book_ids) affected_books = field.table.remove_items(item_ids, self.backend, restrict_to_book_ids=restrict_to_book_ids) if affected_books: if hasattr(field, 'index_field'): self._set_field(field.index_field.name, {bid:1.0 for bid in affected_books}) else: self._mark_as_dirty(affected_books) self._clear_link_map_cache(affected_books) self.event_dispatcher(EventType.items_removed, field, affected_books, item_ids) return affected_books

[documentos] @write_api def add_custom_book_data(self, name, val_map, delete_first=False): ''' Add data for name where val_map is a map of book_ids to values. If delete_first is True, all previously stored data for name will be removed. ''' missing = frozenset(val_map) - self._all_book_ids() if missing: raise ValueError('add_custom_book_data: no such book_ids: %d'%missing) self.backend.add_custom_data(name, val_map, delete_first)

[documentos] @read_api def get_custom_book_data(self, name, book_ids=(), default=None): ''' Get data for name. By default returns data for all book_ids, pass in a list of book ids if you only want some data. Returns a map of book_id to values. If a particular value could not be decoded, uses default for it. ''' return self.backend.get_custom_book_data(name, book_ids, default)

[documentos] @write_api def delete_custom_book_data(self, name, book_ids=()): ''' Delete data for name. By default deletes all data, if you only want to delete data for some book ids, pass in a list of book ids. ''' self.backend.delete_custom_book_data(name, book_ids)

[documentos] @read_api def get_ids_for_custom_book_data(self, name): ''' Return the set of book ids for which name has data. ''' return self.backend.get_ids_for_custom_book_data(name)

@read_api def conversion_options(self, book_id, fmt='PIPE'): return self.backend.conversion_options(book_id, fmt) @read_api def has_conversion_options(self, ids, fmt='PIPE'): return self.backend.has_conversion_options(ids, fmt) @write_api def delete_conversion_options(self, book_ids, fmt='PIPE'): return self.backend.delete_conversion_options(book_ids, fmt)

[documentos] @write_api def set_conversion_options(self, options, fmt='PIPE'): ''' options must be a map of the form {book_id:conversion_options} ''' return self.backend.set_conversion_options(options, fmt)

@write_api def refresh_format_cache(self): self.fields['formats'].table.read(self.backend) self.format_metadata_cache.clear() @write_api def refresh_ondevice(self): self.fields['ondevice'].clear_caches() self.clear_search_caches() self.clear_composite_caches() @read_api def books_matching_device_book(self, lpath): ans = set() for book_id, (_, _, _, _, lpaths) in self.fields['ondevice'].cache.items(): if lpath in lpaths: ans.add(book_id) return ans

[documentos] @read_api def tags_older_than(self, tag, delta=None, must_have_tag=None, must_have_authors=None): ''' Return the ids of all books having the tag ``tag`` that are older than the specified time. tag comparison is case insensitive. :param delta: A timedelta object or None. If None, then all ids with the tag are returned. :param must_have_tag: If not None the list of matches will be restricted to books that have this tag :param must_have_authors: A list of authors. If not None the list of matches will be restricted to books that have these authors (case insensitive). ''' tag_map = {icu_lower(v):k for k, v in iteritems(self._get_id_map('tags'))} tag = icu_lower(tag.strip()) mht = icu_lower(must_have_tag.strip()) if must_have_tag else None tag_id, mht_id = tag_map.get(tag, None), tag_map.get(mht, None) ans = set() if mht_id is None and mht: return ans if tag_id is not None: tagged_books = self._books_for_field('tags', tag_id) if mht_id is not None and tagged_books: tagged_books = tagged_books.intersection(self._books_for_field('tags', mht_id)) if tagged_books: if must_have_authors is not None: amap = {icu_lower(v):k for k, v in iteritems(self._get_id_map('authors'))} books = None for author in must_have_authors: abooks = self._books_for_field('authors', amap.get(icu_lower(author), None)) books = abooks if books is None else books.intersection(abooks) if not books: break tagged_books = tagged_books.intersection(books or set()) if delta is None: ans = tagged_books else: now = nowf() for book_id in tagged_books: ts = self._field_for('timestamp', book_id) if (now - ts) > delta: ans.add(book_id) return ans

@write_api def set_sort_for_authors(self, author_id_to_sort_map, update_books=True): sort_map = self.fields['authors'].table.set_sort_names(author_id_to_sort_map, self.backend) changed_books = set() if update_books: val_map = {} for author_id in sort_map: books = self._books_for_field('authors', author_id) changed_books |= books for book_id in books: authors = self._field_ids_for('authors', book_id) adata = self._author_data(authors) sorts = [adata[x]['sort'] for x in authors] val_map[book_id] = ' & '.join(sorts) if val_map: self._set_field('author_sort', val_map) if changed_books: self._mark_as_dirty(changed_books) self._clear_link_map_cache(changed_books) return changed_books @write_api def set_link_for_authors(self, author_id_to_link_map): link_map = self.fields['authors'].table.set_links(author_id_to_link_map, self.backend) changed_books = set() for author_id in link_map: changed_books |= self._books_for_field('authors', author_id) if changed_books: self._mark_as_dirty(changed_books) self._clear_link_map_cache(changed_books) return changed_books @read_api def has_link_map(self, field): return hasattr(getattr(self.fields.get(field), 'table', None), 'link_map')

[documentos] @read_api def get_link_map(self, for_field): ''' Return a dictionary of links for the supplied field. :param for_field: the lookup name of the field for which the link map is desired :return: {field_value:link_value, ...} for non-empty links ''' if for_field not in self.fields: raise ValueError(f'Lookup name {for_field} is not a valid name') table = self.fields[for_field].table lm = getattr(table, 'link_map', None) if lm is None: raise ValueError(f"Lookup name {for_field} doesn't have a link map") lm = table.link_map vm = table.id_map return {vm.get(fid):v for fid,v in lm.items() if v}

[documentos] @read_api def link_for(self, field, item_id): ''' Return the link, if any, for the specified item or None if no link is found ''' f = self.fields.get(field) if f is not None: table = f.table lm = getattr(table, 'link_map', None) if lm is not None: return lm.get(item_id)

[documentos] @read_api def get_all_link_maps_for_book(self, book_id): ''' Returns all links for all fields referenced by book identified by book_id. If book_id doesn't exist then the method returns {}. Example: Assume author A has link X, author B has link Y, tag S has link F, and tag T has link G. If book 1 has author A and tag T, this method returns {'authors':{'A':'X'}, 'tags':{'T', 'G'}}. If book 2's author is neither A nor B and has no tags, this method returns {}. :param book_id: the book id in question. :return: {field: {field_value, link_value}, ... for all fields with a field_value having a non-empty link value for that book ''' if not self._has_id(book_id): # Works for book_id is None. return {} cached = self.link_maps_cache.get(book_id) if cached is not None: return cached links = {} def add_links_for_field(f): field_ids = self._field_ids_for(f, book_id) if field_ids: table = self.fields[f].table lm = table.link_map id_link_map = {fid:lm.get(fid) for fid in field_ids} vm = table.id_map d = {vm.get(fid):v for fid, v in id_link_map.items() if v} d.pop(None, None) if d: links[f] = d for field in ('authors', 'publisher', 'series', 'tags'): add_links_for_field(field) for field in self.field_metadata.custom_field_keys(include_composites=False): if self._has_link_map(field): add_links_for_field(field) self.link_maps_cache[book_id] = links return links

[documentos] @write_api def set_link_map(self, field, value_to_link_map, only_set_if_no_existing_link=False): ''' Sets links for item values in field. Note: this method doesn't change values not in the value_to_link_map :param field: the lookup name :param value_to_link_map: dict(field_value:link, ...). Note that these are values, not field ids. :return: books changed by setting the link ''' if field not in self.fields: raise ValueError(f'Lookup name {field} is not a valid name') table = getattr(self.fields[field], 'table', None) if table is None: raise ValueError(f"Lookup name {field} doesn't have a link map") # Clear the links for book cache as we don't know what will be affected self.link_maps_cache = {} fids = self._get_item_ids(field, value_to_link_map) if only_set_if_no_existing_link: lm = table.link_map id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None and not lm.get(fid)} else: id_to_link_map = {fid:value_to_link_map[k] for k, fid in fids.items() if fid is not None} result_map = table.set_links(id_to_link_map, self.backend) changed_books = set() for id_ in result_map: changed_books |= self._books_for_field(field, id_) if changed_books: self._mark_as_dirty(changed_books) self._clear_link_map_cache(changed_books) return changed_books

@read_api def lookup_by_uuid(self, uuid): return self.fields['uuid'].table.lookup_by_uuid(uuid) @write_api def delete_custom_column(self, label=None, num=None): self.backend.delete_custom_column(label, num) @write_api def create_custom_column(self, label, name, datatype, is_multiple, editable=True, display={}): return self.backend.create_custom_column(label, name, datatype, is_multiple, editable=editable, display=display) @write_api def set_custom_column_metadata(self, num, name=None, label=None, is_editable=None, display=None, update_last_modified=False): changed = self.backend.set_custom_column_metadata(num, name=name, label=label, is_editable=is_editable, display=display) if changed: if update_last_modified: self._update_last_modified(self._all_book_ids()) else: self.backend.prefs.set('update_all_last_mod_dates_on_start', True) return changed @read_api def get_books_for_category(self, category, item_id_or_composite_value): f = self.fields[category] if hasattr(f, 'get_books_for_val'): # Composite field return f.get_books_for_val(item_id_or_composite_value, self._get_proxy_metadata, self._all_book_ids()) return self._books_for_field(f.name, int(item_id_or_composite_value))

[documentos] @read_api def split_if_is_multiple_composite(self, f, val): ''' If f is a composite column lookup key and the column is is_multiple then split v into unique non-empty values. The comparison is case sensitive. Order is not preserved. Return a list() for compatibility with proxy metadata field getters, for example tags. ''' fm = self.field_metadata.get(f, None) if fm and fm['datatype'] == 'composite' and fm['is_multiple']: sep = fm['is_multiple'].get('cache_to_list', ',') return list({v.strip() for v in val.split(sep) if v.strip()}) return val

[documentos] @read_api def data_for_find_identical_books(self): ''' Return data that can be used to implement :meth:`find_identical_books` in a worker process without access to the db. See db.utils for an implementation. ''' at = self.fields['authors'].table author_map = defaultdict(set) for aid, author in iteritems(at.id_map): author_map[icu_lower(author)].add(aid) return (author_map, at.col_book_map.copy(), self.fields['title'].table.book_col_map.copy(), self.fields['languages'].book_value_map.copy())

@read_api def update_data_for_find_identical_books(self, book_id, data): author_map, author_book_map, title_map, lang_map = data title_map[book_id] = self._field_for('title', book_id) lang_map[book_id] = self._field_for('languages', book_id) at = self.fields['authors'].table for aid in at.book_col_map.get(book_id, ()): author_map[icu_lower(at.id_map[aid])].add(aid) try: author_book_map[aid].add(book_id) except KeyError: author_book_map[aid] = {book_id}

[documentos] @read_api def find_identical_books(self, mi, search_restriction='', book_ids=None): ''' Finds books that have a superset of the authors in mi and the same title (title is fuzzy matched). See also :meth:`data_for_find_identical_books`. ''' from calibre.db.utils import fuzzy_title identical_book_ids = set() langq = tuple(x for x in map(canonicalize_lang, mi.languages or ()) if x and x != 'und') if mi.authors: try: quathors = mi.authors[:20] # Too many authors causes parsing of the search expression to fail query = ' and '.join('authors:"=%s"'%(a.replace('"', '')) for a in quathors) qauthors = mi.authors[20:] except ValueError: return identical_book_ids try: book_ids = self._search(query, restriction=search_restriction, book_ids=book_ids) except: traceback.print_exc() return identical_book_ids if qauthors and book_ids: matches = set() qauthors = {icu_lower(x) for x in qauthors} for book_id in book_ids: aut = self._field_for('authors', book_id) if aut: aut = {icu_lower(x) for x in aut} if aut.issuperset(qauthors): matches.add(book_id) book_ids = matches for book_id in book_ids: fbook_title = self._field_for('title', book_id) fbook_title = fuzzy_title(fbook_title) mbook_title = fuzzy_title(mi.title) if fbook_title == mbook_title: bl = self._field_for('languages', book_id) if not langq or not bl or bl == langq: identical_book_ids.add(book_id) return identical_book_ids

@read_api def get_top_level_move_items(self): all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()} return self.backend.get_top_level_move_items(all_paths) @write_api def move_library_to(self, newloc, progress=None, abort=None): def progress_callback(item_name, item_count, total): try: if progress is not None: progress(item_name, item_count, total) except Exception: traceback.print_exc() all_paths = {self._field_for('path', book_id).partition('/')[0] for book_id in self._all_book_ids()} self.backend.move_library_to(all_paths, newloc, progress=progress_callback, abort=abort) @read_api def saved_search_names(self): return self._search_api.saved_searches.names() @read_api def saved_search_lookup(self, name): return self._search_api.saved_searches.lookup(name) @write_api def saved_search_set_all(self, smap): self._search_api.saved_searches.set_all(smap) self._clear_search_caches() @write_api def saved_search_delete(self, name): self._search_api.saved_searches.delete(name) self._clear_search_caches() @write_api def saved_search_add(self, name, val): self._search_api.saved_searches.add(name, val) @write_api def saved_search_rename(self, old_name, new_name): self._search_api.saved_searches.rename(old_name, new_name) self._clear_search_caches() @write_api def change_search_locations(self, newlocs): self._search_api.change_locations(newlocs) @write_api def refresh_search_locations(self): self._search_api.change_locations(self.field_metadata.get_search_terms()) @write_api def dump_and_restore(self, callback=None, sql=None): return self.backend.dump_and_restore(callback=callback, sql=sql) @write_api def vacuum(self, include_fts_db=False, include_notes_db=True): self.is_doing_rebuild_or_vacuum = True try: self.backend.vacuum(include_fts_db, include_notes_db) finally: self.is_doing_rebuild_or_vacuum = False def __del__(self): self.close() def _shutdown_fts(self, stage=1): if stage == 1: self.backend.shutdown_fts() if self.fts_queue_thread is not None: self.fts_job_queue.put(None) if hasattr(self, 'fts_dispatch_stop_event'): self.fts_dispatch_stop_event.set() return # the fts supervisor thread could be in the middle of committing a # result to the db, so holding a lock here will cause a deadlock if self.fts_queue_thread is not None: self.fts_queue_thread.join() self.fts_queue_thread = None self.backend.join_fts() @api def close(self): with self.write_lock: if hasattr(self, 'close_called'): return self.close_called = True self.shutting_down = True self.event_dispatcher.close() self._shutdown_fts() try: from calibre.customize.ui import available_library_closed_plugins except ImportError: pass # happens during interpreter shutdown else: for plugin in available_library_closed_plugins(): try: plugin.run(self) except Exception: traceback.print_exc() self._shutdown_fts(stage=2) with self.write_lock: self.backend.close() @property def is_closed(self): return self.backend.is_closed @write_api def clear_trash_bin(self): self.backend.clear_trash_dir() @read_api def list_trash_entries(self): books, formats = self.backend.list_trash_entries() ff = [] for e in formats: if self._has_id(e.book_id): ff.append(e) e.cover_path = self.format_abspath(e.book_id, '__COVER_INTERNAL__') return books, formats @read_api def copy_format_from_trash(self, book_id, fmt, dest): fmt = fmt.upper() fpath = self.backend.path_for_trash_format(book_id, fmt) if not fpath: raise ValueError(f'No format {fmt} found in book {book_id}') shutil.copyfile(fpath, dest)

[documentos] @write_api def move_format_from_trash(self, book_id, fmt): ''' Undelete a format from the trash directory ''' if not self._has_id(book_id): raise ValueError(f'A book with the id {book_id} does not exist') fmt = fmt.upper() try: name = self.fields['formats'].format_fname(book_id, fmt) except Exception: name = None fpath = self.backend.path_for_trash_format(book_id, fmt) if not fpath: raise ValueError(f'No format {fmt} found in book {book_id}') size, fname = self._do_add_format(book_id, fmt, fpath, name) self.format_metadata_cache.pop(book_id, None) max_size = self.fields['formats'].table.update_fmt(book_id, fmt, fname, size, self.backend) self.fields['size'].table.update_sizes({book_id: max_size}) self.event_dispatcher(EventType.format_added, book_id, fmt) self.backend.remove_trash_formats_dir_if_empty(book_id)

@read_api def copy_book_from_trash(self, book_id, dest: str): self.backend.copy_book_from_trash(book_id, dest)

[documentos] @write_api def move_book_from_trash(self, book_id): ''' Undelete a book from the trash directory ''' if self._has_id(book_id): raise ValueError(f'A book with the id {book_id} already exists') mi, annotations, formats = self.backend.get_metadata_for_trash_book(book_id) mi.cover = None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True) path = self._field_for('path', book_id).replace('/', os.sep) self.backend.move_book_from_trash(book_id, path) self.format_metadata_cache.pop(book_id, None) f = self.fields['formats'].table max_size = 0 for (fmt, size, fname) in formats: max_size = max(max_size, f.update_fmt(book_id, fmt, fname, size, self.backend)) self.fields['size'].table.update_sizes({book_id: max_size}) cover = self.backend.cover_abspath(book_id, path) if cover and os.path.exists(cover): self._set_field('cover', {book_id:1}) if annotations: self._restore_annotations(book_id, annotations)

[documentos] @write_api def delete_trash_entry(self, book_id, category): " Delete an entry from the trash. Here category is 'b' for books and 'f' for formats. " self.backend.delete_trash_entry(book_id, category)

[documentos] @write_api def expire_old_trash(self): ' Expire entries from the trash that are too old ' self.backend.expire_old_trash()

[documentos] @write_api def restore_book(self, book_id, mi, last_modified, path, formats, annotations=()): ''' Restore the book entry in the database for a book that already exists on the filesystem ''' cover, mi.cover = mi.cover, None self._create_book_entry(mi, add_duplicates=True, force_id=book_id, apply_import_tags=False, preserve_uuid=True) self._update_last_modified((book_id,), last_modified) if cover and os.path.exists(cover): self._set_field('cover', {book_id:1}) f = self.fields['formats'].table for (fmt, size, fname) in formats: f.update_fmt(book_id, fmt, fname, size, self.backend) self.fields['path'].table.set_path(book_id, path, self.backend) if annotations: self._restore_annotations(book_id, annotations)

@read_api def virtual_libraries_for_books(self, book_ids, virtual_fields=None): # use a primitive lock to ensure that only one thread is updating # the cache and that recursive calls don't do the update. This # method can recurse via self._search() with try_lock(self.vls_cache_lock) as got_lock: # Using a list is slightly faster than a set. c = defaultdict(list) if not got_lock: # We get here if resolving the books in a VL triggers another VL # cache calculation. This can be 'real' recursion, for example a # VL expression using a template that calls virtual_libraries(), # or a search using a location of 'all' that causes evaluation # of a composite that uses virtual_libraries(). The first case # is an error and the exception message should appear somewhere. # However, the error can seem nondeterministic. It might not be # raised if the use is via a composite and that composite is # evaluated before it is used in the search. The second case is # also an error but if the composite isn't used in a VL then the # eventual answer will be correct because get_metadata() will # clear the caches. raise ValueError(_('Recursion detected while processing Virtual library "%s"') % self.vls_for_books_lib_in_process) if self.vls_for_books_cache is None: libraries = self._pref('virtual_libraries', {}) for lib, expr in libraries.items(): book = None self.vls_for_books_lib_in_process = lib try: for book in self._search(expr, virtual_fields=virtual_fields): c[book].append(lib) except Exception as e: if book: c[book].append(_('[Error in Virtual library {0}: {1}]').format(lib, str(e))) self.vls_for_books_cache = {b:tuple(sorted(libs, key=sort_key)) for b, libs in c.items()} if not book_ids: book_ids = self._all_book_ids() # book_ids is usually 1 long. The loop will be faster than a comprehension r = {} default = () for b in book_ids: r[b] = self.vls_for_books_cache.get(b, default) return r

[documentos] @read_api def user_categories_for_books(self, book_ids, proxy_metadata_map=None): ''' Return the user categories for the specified books. proxy_metadata_map is optional and is useful for a performance boost, in contexts where a ProxyMetadata object for the books already exists. It should be a mapping of book_ids to their corresponding ProxyMetadata objects. ''' user_cats = self._pref('user_categories', {}) pmm = proxy_metadata_map or {} ans = {} for book_id in book_ids: proxy_metadata = pmm.get(book_id) or self._get_proxy_metadata(book_id) user_cat_vals = ans[book_id] = {} for ucat, categories in iteritems(user_cats): user_cat_vals[ucat] = res = [] for name, cat, ign in categories: try: field_obj = self.fields[cat] except KeyError: continue if field_obj.is_composite: v = field_obj.get_value_with_cache(book_id, lambda x:proxy_metadata) else: v = self._fast_field_for(field_obj, book_id) if isinstance(v, (list, tuple)): if name in v: res.append([name, cat]) elif name == v: res.append([name, cat]) return ans

[documentos] @write_api def embed_metadata(self, book_ids, only_fmts=None, report_error=None, report_progress=None): ''' Update metadata in all formats of the specified book_ids to current metadata in the database. ''' field = self.fields['formats'] from calibre.customize.ui import apply_null_metadata from calibre.ebooks.metadata.meta import set_metadata from calibre.ebooks.metadata.opf2 import pretty_print if only_fmts: only_fmts = {f.lower() for f in only_fmts} def doit(fmt, mi, stream): with apply_null_metadata, pretty_print: set_metadata(stream, mi, stream_type=fmt, report_error=report_error) stream.seek(0, os.SEEK_END) return stream.tell() for i, book_id in enumerate(book_ids): fmts = field.table.book_col_map.get(book_id, ()) if not fmts: continue mi = self._get_metadata(book_id) buf = BytesIO() if not self._copy_cover_to(book_id, buf): return cdata = buf.getvalue() if cdata: mi.cover_data = ('jpeg', cdata) try: path = self._field_for('path', book_id).replace('/', os.sep) except: continue for fmt in fmts: if only_fmts is not None and fmt.lower() not in only_fmts: continue try: name = self.fields['formats'].format_fname(book_id, fmt) except: continue if name and path: try: new_size = self.backend.apply_to_format(book_id, path, name, fmt, partial(doit, fmt, mi)) except Exception as e: if report_error is not None: tb = traceback.format_exc() if iswindows and isinstance(e, PermissionError) and e.filename and isinstance(e.filename, str): from calibre_extensions import winutil try: p = winutil.get_processes_using_files(e.filename) except OSError: pass else: path_map = {x['path']: x for x in p} tb = _('Could not open the file: "{}". It is already opened in the following programs:').format(e.filename) for path, x in path_map.items(): tb += '\n' + f'{x["app_name"]}: {path}' report_error(mi, fmt, tb) new_size = None else: raise if new_size is not None: self.format_metadata_cache[book_id].get(fmt, {})['size'] = new_size max_size = self.fields['formats'].table.update_fmt(book_id, fmt, name, new_size, self.backend) self.fields['size'].table.update_sizes({book_id: max_size}) if report_progress is not None: report_progress(i+1, len(book_ids), mi)

@read_api def get_last_read_positions(self, book_id, fmt, user): fmt = fmt.upper() ans = [] for device, cfi, epoch, pos_frac in self.backend.execute( 'SELECT device,cfi,epoch,pos_frac FROM last_read_positions WHERE book=? AND format=? AND user=?', (book_id, fmt, user)): ans.append({'device':device, 'cfi': cfi, 'epoch':epoch, 'pos_frac':pos_frac}) return ans @write_api def set_last_read_position(self, book_id, fmt, user='_', device='_', cfi=None, epoch=None, pos_frac=0): fmt = fmt.upper() device = device or '_' user = user or '_' if not cfi: self.backend.execute( 'DELETE FROM last_read_positions WHERE book=? AND format=? AND user=? AND device=?', (book_id, fmt, user, device)) else: self.backend.execute( 'INSERT OR REPLACE INTO last_read_positions(book,format,user,device,cfi,epoch,pos_frac) VALUES (?,?,?,?,?,?,?)', (book_id, fmt, user, device, cfi, epoch or time(), pos_frac)) @read_api def export_library(self, library_key, exporter, progress=None, abort=None): from polyglot.binary import as_hex_unicode key_prefix = as_hex_unicode(library_key) book_ids = self._all_book_ids() total = len(book_ids) + 2 has_fts = self.is_fts_enabled() if has_fts: total += 1 poff = 0 def report_progress(fname): nonlocal poff if progress is not None: progress(fname, poff, total) poff += 1 report_progress('metadata.db') pt = PersistentTemporaryFile('-export.db') pt.close() self.backend.backup_database(pt.name) dbkey = key_prefix + ':::' + 'metadata.db' with open(pt.name, 'rb') as f: exporter.add_file(f, dbkey) os.remove(pt.name) if has_fts: report_progress('full-text-search.db') pt = PersistentTemporaryFile('-export.db') pt.close() self.backend.backup_fts_database(pt.name) ftsdbkey = key_prefix + ':::full-text-search.db' with open(pt.name, 'rb') as f: exporter.add_file(f, ftsdbkey) os.remove(pt.name) notesdbkey = key_prefix + ':::notes.db' with PersistentTemporaryFile('-export.db') as pt: self.backend.export_notes_data(pt) pt.flush() pt.seek(0) report_progress('notes.db') exporter.add_file(pt, notesdbkey) format_metadata = {} extra_files = {} metadata = {'format_data':format_metadata, 'metadata.db':dbkey, 'notes.db': notesdbkey, 'total':total, 'extra_files': extra_files} if has_fts: metadata['full-text-search.db'] = ftsdbkey for i, book_id in enumerate(book_ids): if abort is not None and abort.is_set(): return if progress is not None: report_progress(self._field_for('title', book_id)) format_metadata[book_id] = fm = {} for fmt in self._formats(book_id): mdata = self.format_metadata(book_id, fmt) key = f'{key_prefix}:{book_id}:{fmt}' fm[fmt] = key with exporter.start_file(key, mtime=mdata.get('mtime')) as dest: self._copy_format_to(book_id, fmt, dest, report_file_size=dest.ensure_space) cover_key = '{}:{}:{}'.format(key_prefix, book_id, '.cover') with exporter.start_file(cover_key) as dest: if not self.copy_cover_to(book_id, dest, report_file_size=dest.ensure_space): dest.discard() else: fm['.cover'] = cover_key bp = self.field_for('path', book_id) extra_files[book_id] = ef = {} if bp: for (relpath, fobj, stat_result) in self.backend.iter_extra_files(book_id, bp, self.fields['formats']): key = f'{key_prefix}:{book_id}:.|{relpath}' with exporter.start_file(key, mtime=stat_result.st_mtime) as dest: shutil.copyfileobj(fobj, dest) ef[relpath] = key exporter.set_metadata(library_key, metadata) if progress is not None: progress(_('Completed'), total, total) @read_api def annotations_map_for_book(self, book_id, fmt, user_type='local', user='viewer'): ans = {} for annot in self.backend.annotations_for_book(book_id, fmt, user_type, user): ans.setdefault(annot['type'], []).append(annot) return ans @read_api def all_annotations_for_book(self, book_id): return tuple(self.backend.all_annotations_for_book(book_id)) @read_api def annotation_count_for_book(self, book_id): return self.backend.annotation_count_for_book(book_id) @read_api def all_annotation_users(self): return tuple(self.backend.all_annotation_users()) @read_api def all_annotation_types(self): return tuple(self.backend.all_annotation_types()) @read_api def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False, restrict_to_book_ids=None): return tuple(self.backend.all_annotations(restrict_to_user, limit, annotation_type, ignore_removed, restrict_to_book_ids)) @read_api def search_annotations( self, fts_engine_query, use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, annotation_type=None, restrict_to_book_ids=None, restrict_to_user=None, ignore_removed=False ): return tuple(self.backend.search_annotations( fts_engine_query, use_stemming, highlight_start, highlight_end, snippet_size, annotation_type, restrict_to_book_ids, restrict_to_user, ignore_removed )) @write_api def delete_annotations(self, annot_ids): self.backend.delete_annotations(annot_ids) @write_api def update_annotations(self, annot_id_map): self.backend.update_annotations(annot_id_map) @write_api def restore_annotations(self, book_id, annotations): from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 umap = defaultdict(list) for adata in annotations: key = adata['user_type'], adata['user'], adata['format'] a = adata['annotation'] ts = (parse_iso8601(a['timestamp']) - EPOCH).total_seconds() umap[key].append((a, ts)) for (user_type, user, fmt), annots_list in iteritems(umap): self._set_annotations_for_book(book_id, fmt, annots_list, user_type=user_type, user=user) @write_api def set_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'): self.backend.set_annotations_for_book(book_id, fmt, annots_list, user_type, user) @write_api def merge_annotations_for_book(self, book_id, fmt, annots_list, user_type='local', user='viewer'): from calibre.utils.date import EPOCH from calibre.utils.iso8601 import parse_iso8601 amap = self._annotations_map_for_book(book_id, fmt, user_type=user_type, user=user) merge_annotations(annots_list, amap) alist = [] for val in itervalues(amap): for annot in val: ts = (parse_iso8601(annot['timestamp']) - EPOCH).total_seconds() alist.append((annot, ts)) self._set_annotations_for_book(book_id, fmt, alist, user_type=user_type, user=user) @write_api def reindex_annotations(self): self.backend.reindex_annotations() @read_api def are_paths_inside_book_dir(self, book_id, paths, sub_path=''): try: path = self._field_for('path', book_id).replace('/', os.sep) except: return set() return {x for x in paths if self.backend.is_path_inside_book_dir(x, path, sub_path)}

[documentos] @write_api def add_extra_files(self, book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False): ' Add extra data files ' path = self._field_for('path', book_id).replace('/', os.sep) added = {} for relpath, stream_or_path in map_of_relpath_to_stream_or_path.items(): added[relpath] = bool(self.backend.add_extra_file(relpath, stream_or_path, path, replace, auto_rename)) self._clear_extra_files_cache(book_id) return added

[documentos] @write_api def rename_extra_files(self, book_id, map_of_relpath_to_new_relpath, replace=False): ' Rename extra data files ' path = self._field_for('path', book_id).replace('/', os.sep) renamed = set() for relpath, newrelpath in map_of_relpath_to_new_relpath.items(): if self.backend.rename_extra_file(relpath, newrelpath, path, replace): renamed.add(relpath) self._clear_extra_files_cache(book_id) return renamed

[documentos] @write_api def merge_extra_files(self, dest_id, src_ids, replace=False): ' Merge the extra files from src_ids into dest_id. Conflicting files are auto-renamed unless replace=True in which case they are replaced. ' added = set() path = self._field_for('path', dest_id) if path: path = path.replace('/', os.sep) for src_id in src_ids: book_path = self._field_for('path', src_id) if book_path: book_path = book_path.replace('/', os.sep) for (relpath, file_path, stat_result) in self.backend.iter_extra_files( src_id, book_path, self.fields['formats'], yield_paths=True): added.add(self.backend.add_extra_file(relpath, file_path, path, replace=replace, auto_rename=True)) self._clear_extra_files_cache(dest_id) return added

[documentos] @read_api def list_extra_files(self, book_id, use_cache=False, pattern='') -> Tuple[ExtraFile, ...]: ''' Get information about extra files in the book's directory. :param book_id: the database book id for the book :param pattern: the pattern of filenames to search for. Empty pattern matches all extra files. Patterns must use / as separator. Use the DATA_FILE_PATTERN constant to match files inside the data directory. :return: A tuple of all extra files matching the specified pattern. Each element of the tuple is ExtraFile(relpath, file_path, stat_result). Where relpath is the relative path of the file to the book directory using / as a separator. stat_result is the result of calling os.stat() on the file. ''' ans = self.extra_files_cache.setdefault(book_id, {}).get(pattern) if ans is None or not use_cache: ans = [] path = self._field_for('path', book_id) if path: for (relpath, file_path, stat_result) in self.backend.iter_extra_files( book_id, path, self.fields['formats'], yield_paths=True, pattern=pattern ): ans.append(ExtraFile(relpath, file_path, stat_result)) self.extra_files_cache[book_id][pattern] = ans = tuple(ans) return ans

@read_api def copy_extra_file_to(self, book_id, relpath, stream_or_path): path = self._field_for('path', book_id).replace('/', os.sep) self.backend.copy_extra_file_to(book_id, path, relpath, stream_or_path) @write_api def merge_book_metadata(self, dest_id, src_ids, replace_cover=False): dest_mi = self.get_metadata(dest_id) merged_identifiers = self._field_for('identifiers', dest_id) or {} orig_dest_comments = dest_mi.comments dest_cover = orig_dest_cover = self.cover(dest_id) had_orig_cover = bool(dest_cover) from calibre.utils.date import is_date_undefined def is_null_date(x): return x is None or is_date_undefined(x) for src_id in src_ids: src_mi = self.get_metadata(src_id) if src_mi.comments and orig_dest_comments != src_mi.comments: if not dest_mi.comments: dest_mi.comments = src_mi.comments else: dest_mi.comments = str(dest_mi.comments) + '\n\n' + str(src_mi.comments) if src_mi.title and dest_mi.is_null('title'): dest_mi.title = src_mi.title dest_mi.title_sort = src_mi.title_sort if (src_mi.authors and src_mi.authors[0] != _('Unknown')) and (not dest_mi.authors or dest_mi.authors[0] == _('Unknown')): dest_mi.authors = src_mi.authors dest_mi.author_sort = src_mi.author_sort if src_mi.tags: if not dest_mi.tags: dest_mi.tags = src_mi.tags else: dest_mi.tags.extend(src_mi.tags) if not dest_cover or replace_cover: src_cover = self.cover(src_id) if src_cover: dest_cover = src_cover replace_cover = False if not dest_mi.publisher: dest_mi.publisher = src_mi.publisher if not dest_mi.rating: dest_mi.rating = src_mi.rating if not dest_mi.series: dest_mi.series = src_mi.series dest_mi.series_index = src_mi.series_index if is_null_date(dest_mi.pubdate) and not is_null_date(src_mi.pubdate): dest_mi.pubdate = src_mi.pubdate src_identifiers = self.field_for('identifier', src_id) or {} src_identifiers.update(merged_identifiers) merged_identifiers = src_identifiers.copy() if merged_identifiers: dest_mi.set_identifiers(merged_identifiers) self._set_metadata(dest_id, dest_mi, ignore_errors=False) if dest_cover and (not had_orig_cover or dest_cover is not orig_dest_cover): self._set_cover({dest_id: dest_cover}) for key in self.field_metadata: # loop thru all defined fields fm = self.field_metadata[key] if not fm['is_custom']: continue dt = fm['datatype'] label = fm['label'] try: field = self.field_metadata.label_to_key(label) except ValueError: continue # Get orig_dest_comments before it gets changed if dt == 'comments': orig_dest_value = self._field_for(field, dest_id) for src_id in src_ids: dest_value = self._field_for(field, dest_id) src_value = self._field_for(field, src_id) if (dt == 'comments' and src_value and src_value != orig_dest_value): if not dest_value: self._set_field(field, {dest_id: src_value}) else: dest_value = str(dest_value) + '\n\n' + str(src_value) self._set_field(field, {dest_id: dest_value}) if (dt in {'bool', 'int', 'float', 'rating', 'datetime'} and dest_value is None): self._set_field(field, {dest_id: src_value}) if (dt == 'series' and not dest_value and src_value): src_index = self._field_for(field + '_index', src_id) self._set_field(field, {dest_id:src_value}) self._set_field(field + '_index', {dest_id:src_index}) if ((dt == 'enumeration' or (dt == 'text' and not fm['is_multiple'])) and not dest_value): self._set_field(field, {dest_id:src_value}) if (dt == 'text' and fm['is_multiple'] and src_value): if not dest_value: dest_value = src_value else: dest_value = list(dest_value) dest_value.extend(src_value) self._set_field(field, {dest_id: dest_value})

def import_library(library_key, importer, library_path, progress=None, abort=None):
    from calibre.db.backend import DB
    metadata = importer.metadata[library_key]
    total = metadata['total']
    poff = 0
    def report_progress(fname):
        nonlocal poff
        if progress is not None:
            progress(fname, poff, total)
            poff += 1
    report_progress('metadata.db')
    if abort is not None and abort.is_set():
        return
    with open(os.path.join(library_path, 'metadata.db'), 'wb') as f:
        src = importer.start_file(metadata['metadata.db'], 'metadata.db for ' + library_path)
        shutil.copyfileobj(src, f)
        src.close()
    if 'full-text-search.db' in metadata:
        if progress is not None:
            progress('full-text-search.db', 1, total)
        if abort is not None and abort.is_set():
            return
        poff += 1
        with open(os.path.join(library_path, 'full-text-search.db'), 'wb') as f:
            src = importer.start_file(metadata['full-text-search.db'], 'full-text-search.db for ' + library_path)
            shutil.copyfileobj(src, f)
            src.close()
    if abort is not None and abort.is_set():
        return
    if 'notes.db' in metadata:
        import zipfile
        notes_dir = os.path.join(library_path, NOTES_DIR_NAME)
        os.makedirs(notes_dir, exist_ok=True)
        with closing(importer.start_file(metadata['notes.db'], 'notes.db for ' + library_path)) as stream:
            stream.check_hash = False
            with zipfile.ZipFile(stream) as zf:
                for zi in zf.infolist():
                    tpath = zf._extract_member(zi, notes_dir, None)
                    date_time = mktime(zi.date_time + (0, 0, -1))
                    os.utime(tpath, (date_time, date_time))
    if abort is not None and abort.is_set():
        return
    cache = Cache(DB(library_path, load_user_formatter_functions=False))
    cache.init()

    format_data = {int(book_id):data for book_id, data in iteritems(metadata['format_data'])}
    extra_files = {int(book_id):data for book_id, data in metadata.get('extra_files', {}).items()}
    for i, (book_id, fmt_key_map) in enumerate(iteritems(format_data)):
        if abort is not None and abort.is_set():
            return
        title = cache._field_for('title', book_id)
        if progress is not None:
            progress(title, i + poff, total)
        cache._update_path((book_id,), mark_as_dirtied=False)
        for fmt, fmtkey in iteritems(fmt_key_map):
            if fmt == '.cover':
                with closing(importer.start_file(fmtkey, _('Cover for %s') % title)) as stream:
                    path = cache._field_for('path', book_id).replace('/', os.sep)
                    cache.backend.set_cover(book_id, path, stream, no_processing=True)
            else:
                with closing(importer.start_file(fmtkey, _('{0} format for {1}').format(fmt.upper(), title))) as stream:
                    size, fname = cache._do_add_format(book_id, fmt, stream, mtime=stream.mtime)
                    cache.fields['formats'].table.update_fmt(book_id, fmt, fname, size, cache.backend)
        for relpath, efkey in extra_files.get(book_id, {}).items():
            with closing(importer.start_file(efkey, _('Extra file {0} for book {1}').format(relpath, title))) as stream:
                path = cache._field_for('path', book_id).replace('/', os.sep)
                cache.backend.add_extra_file(relpath, stream, path)
        cache.dump_metadata({book_id})
    if progress is not None:
        progress(_('Completed'), total, total)
    return cache
# }}}

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.devices.interface

Código fuente para calibre.devices.interface

__license__   = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from collections import namedtuple

from calibre import prints
from calibre.constants import iswindows
from calibre.customize import Plugin

class OpenPopupMessage:

    def __init__(self, title='', message='', level='info', skip_dialog_skip_precheck=True):
        self.title = title
        self.message = message
        self.level = level
        self.skip_dialog_skip_precheck = skip_dialog_skip_precheck

[documentos] class DevicePlugin(Plugin): """ Defines the interface that should be implemented by backends that communicate with an e-book reader. """ type = _('Device interface') #: Ordered list of supported formats FORMATS = ["lrf", "rtf", "pdf", "txt"] # If True, the config dialog will not show the formats box HIDE_FORMATS_CONFIG_BOX = False #: VENDOR_ID can be either an integer, a list of integers or a dictionary #: If it is a dictionary, it must be a dictionary of dictionaries, #: of the form:: #: #: { #: integer_vendor_id : { product_id : [list of BCDs], ... }, #: ... #: } #: VENDOR_ID = 0x0000 #: An integer or a list of integers PRODUCT_ID = 0x0000 #: BCD can be either None to not distinguish between devices based on BCD, or #: it can be a list of the BCD numbers of all devices supported by this driver. BCD = None #: Height for thumbnails on the device THUMBNAIL_HEIGHT = 68 #: Compression quality for thumbnails. Set this closer to 100 to have better #: quality thumbnails with fewer compression artifacts. Of course, the #: thumbnails get larger as well. THUMBNAIL_COMPRESSION_QUALITY = 75 #: Set this to True if the device supports updating cover thumbnails during #: sync_booklists. Setting it to true will ask device.py to refresh the #: cover thumbnails during book matching WANTS_UPDATED_THUMBNAILS = False #: Whether the metadata on books can be set via the GUI. CAN_SET_METADATA = ['title', 'authors', 'collections'] #: Whether the device can handle device_db metadata plugboards CAN_DO_DEVICE_DB_PLUGBOARD = False # Set this to None if the books on the device are files that the GUI can # access in order to add the books from the device to the library BACKLOADING_ERROR_MESSAGE = _('Cannot get files from this device') #: Path separator for paths to books on device path_sep = os.sep #: Icon for this device icon = 'reader.png' # Encapsulates an annotation fetched from the device UserAnnotation = namedtuple('Annotation','type, value') #: GUI displays this as a message if not None in the status bar. Useful if opening can take a #: long time OPEN_FEEDBACK_MESSAGE = None #: Set of extensions that are "virtual books" on the device #: and therefore cannot be viewed/saved/added to library. #: For example: ``frozenset(['kobo'])`` VIRTUAL_BOOK_EXTENSIONS = frozenset() #: Message to display to user for virtual book extensions. VIRTUAL_BOOK_EXTENSION_MESSAGE = None #: Whether to nuke comments in the copy of the book sent to the device. If #: not None this should be short string that the comments will be replaced #: by. NUKE_COMMENTS = None #: If True indicates that this driver completely manages device detection, #: ejecting and so forth. If you set this to True, you *must* implement the #: detect_managed_devices and debug_managed_device_detection methods. #: A driver with this set to true is responsible for detection of devices, #: managing a blacklist of devices, a list of ejected devices and so forth. #: calibre will periodically call the detect_managed_devices() method and #: if it returns a detected device, calibre will call open(). open() will #: be called every time a device is returned even if previous calls to open() #: failed, therefore the driver must maintain its own blacklist of failed #: devices. Similarly, when ejecting, calibre will call eject() and then #: assuming the next call to detect_managed_devices() returns None, it will #: call post_yank_cleanup(). MANAGES_DEVICE_PRESENCE = False #: If set the True, calibre will call the :meth:`get_driveinfo()` method #: after the books lists have been loaded to get the driveinfo. SLOW_DRIVEINFO = False #: If set to True, calibre will ask the user if they want to manage the #: device with calibre, the first time it is detected. If you set this to #: True you must implement :meth:`get_device_uid()` and #: :meth:`ignore_connected_device()` and #: :meth:`get_user_blacklisted_devices` and #: :meth:`set_user_blacklisted_devices` ASK_TO_ALLOW_CONNECT = False #: Set this to a dictionary of the form {'title':title, 'msg':msg, 'det_msg':detailed_msg} to have calibre popup #: a message to the user after some callbacks are run (currently only upload_books). #: Be careful to not spam the user with too many messages. This variable is checked after *every* callback, #: so only set it when you really need to. user_feedback_after_callback = None @classmethod def get_gui_name(cls): if hasattr(cls, 'gui_name'): return cls.gui_name if hasattr(cls, '__name__'): return cls.__name__ return cls.name

[documentos] @classmethod def get_open_popup_message(self): ' GUI displays this as a non-modal popup. Should be an instance of OpenPopupMessage ' return None

# Device detection {{{ def test_bcd(self, bcdDevice, bcd): if bcd is None or len(bcd) == 0: return True for c in bcd: if c == bcdDevice: return True return False

[documentos] def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): ''' Return True, device_info if a device handled by this plugin is currently connected. :param devices_on_system: List of devices currently connected ''' vendors_on_system = {x[0] for x in devices_on_system} vendors = set(self.VENDOR_ID) if hasattr(self.VENDOR_ID, '__len__') else {self.VENDOR_ID} if hasattr(self.VENDOR_ID, 'keys'): products = [] for ven in self.VENDOR_ID: products.extend(self.VENDOR_ID[ven].keys()) else: products = self.PRODUCT_ID if hasattr(self.PRODUCT_ID, '__len__') else [self.PRODUCT_ID] ch = self.can_handle_windows if iswindows else self.can_handle for vid in vendors_on_system.intersection(vendors): for dev in devices_on_system: cvid, pid, bcd = dev[:3] if cvid == vid: if pid in products: if hasattr(self.VENDOR_ID, 'keys'): try: cbcd = self.VENDOR_ID[vid][pid] except KeyError: # Vendor vid does not have product pid, pid # exists for some other vendor in this # device continue else: cbcd = self.BCD if self.test_bcd(bcd, cbcd): if debug: prints(dev) if ch(dev, debug=debug): return True, dev return False, None

[documentos] def detect_managed_devices(self, devices_on_system, force_refresh=False): ''' Called only if MANAGES_DEVICE_PRESENCE is True. Scan for devices that this driver can handle. Should return a device object if a device is found. This object will be passed to the open() method as the connected_device. If no device is found, return None. The returned object can be anything, calibre does not use it, it is only passed to open(). This method is called periodically by the GUI, so make sure it is not too resource intensive. Use a cache to avoid repeatedly scanning the system. :param devices_on_system: Set of USB devices found on the system. :param force_refresh: If True and the driver uses a cache to prevent repeated scanning, the cache must be flushed. ''' raise NotImplementedError()

[documentos] def debug_managed_device_detection(self, devices_on_system, output): ''' Called only if MANAGES_DEVICE_PRESENCE is True. Should write information about the devices detected on the system to output, which is a file like object. Should return True if a device was detected and successfully opened, otherwise False. ''' raise NotImplementedError()

# }}}

[documentos] def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): """ :param key: The key to unlock the device :param log_packets: If true the packet stream to/from the device is logged :param report_progress: Function that is called with a % progress (number between 0 and 100) for various tasks. If it is called with -1 that means that the task does not have any progress information :param detected_device: Device information from the device scanner """ raise NotImplementedError()

[documentos] def can_handle_windows(self, usbdevice, debug=False): ''' Optional method to perform further checks on a device to see if this driver is capable of handling it. If it is not it should return False. This method is only called after the vendor, product ids and the bcd have matched, so it can do some relatively time intensive checks. The default implementation returns True. This method is called only on Windows. See also :meth:`can_handle`. Note that for devices based on USBMS this method by default delegates to :meth:`can_handle`. So you only need to override :meth:`can_handle` in your subclass of USBMS. :param usbdevice: A usbdevice as returned by :func:`calibre.devices.winusb.scan_usb_devices` ''' return True

[documentos] def can_handle(self, device_info, debug=False): ''' Unix version of :meth:`can_handle_windows`. :param device_info: Is a tuple of (vid, pid, bcd, manufacturer, product, serial number) ''' return True

can_handle.is_base_class_implementation = True

[documentos] def open(self, connected_device, library_uuid): ''' Perform any device specific initialization. Called after the device is detected but before any other functions that communicate with the device. For example: For devices that present themselves as USB Mass storage devices, this method would be responsible for mounting the device or if the device has been automounted, for finding out where it has been mounted. The method :meth:`calibre.devices.usbms.device.Device.open` has an implementation of this function that should serve as a good example for USB Mass storage devices. This method can raise an OpenFeedback exception to display a message to the user. :param connected_device: The device that we are trying to open. It is a tuple of (vendor id, product id, bcd, manufacturer name, product name, device serial number). However, some devices have no serial number and on Windows only the first three fields are present, the rest are None. :param library_uuid: The UUID of the current calibre library. Can be None if there is no library (for example when used from the command line). ''' raise NotImplementedError()

[documentos] def eject(self): ''' Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device. NOTE: That this method may not be called on the same thread as the rest of the device methods. ''' raise NotImplementedError()

[documentos] def post_yank_cleanup(self): ''' Called if the user yanks the device without ejecting it first. ''' raise NotImplementedError()

[documentos] def set_progress_reporter(self, report_progress): ''' Set a function to report progress information. :param report_progress: Function that is called with a % progress (number between 0 and 100) for various tasks. If it is called with -1 that means that the task does not have any progress information ''' raise NotImplementedError()

[documentos] def get_device_information(self, end_session=True): """ Ask device for device information. See L{DeviceInfoQuery}. :return: (device name, device version, software version on device, MIME type) The tuple can optionally have a fifth element, which is a drive information dictionary. See usbms.driver for an example. """ raise NotImplementedError()

[documentos] def get_driveinfo(self): ''' Return the driveinfo dictionary. Usually called from get_device_information(), but if loading the driveinfo is slow for this driver, then it should set SLOW_DRIVEINFO. In this case, this method will be called by calibre after the book lists have been loaded. Note that it is not called on the device thread, so the driver should cache the drive info in the books() method and this function should return the cached data. ''' return {}

[documentos] def card_prefix(self, end_session=True): ''' Return a 2 element list of the prefix to paths on the cards. If no card is present None is set for the card's prefix. E.G. ('/place', '/place2') (None, 'place2') ('place', None) (None, None) ''' raise NotImplementedError()

[documentos] def total_space(self, end_session=True): """ Get total space available on the mountpoints: 1. Main memory 2. Memory Card A 3. Memory Card B :return: A 3 element list with total space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return 0. """ raise NotImplementedError()

[documentos] def free_space(self, end_session=True): """ Get free space available on the mountpoints: 1. Main memory 2. Card A 3. Card B :return: A 3 element list with free space in bytes of (1, 2, 3). If a particular device doesn't have any of these locations it should return -1. """ raise NotImplementedError()

[documentos] def books(self, oncard=None, end_session=True): """ Return a list of e-books on the device. :param oncard: If 'carda' or 'cardb' return a list of e-books on the specific storage card, otherwise return list of e-books in main memory of device. If a card is specified and no books are on the card return empty list. :return: A BookList. """ raise NotImplementedError()

[documentos] def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): ''' Upload a list of books to the device. If a file already exists on the device, it should be replaced. This method should raise a :class:`FreeSpaceError` if there is not enough free space on the device. The text of the FreeSpaceError must contain the word "card" if ``on_card`` is not None otherwise it must contain the word "memory". :param files: A list of paths :param names: A list of file names that the books should have once uploaded to the device. len(names) == len(files) :param metadata: If not None, it is a list of :class:`Metadata` objects. The idea is to use the metadata to determine where on the device to put the book. len(metadata) == len(files). Apart from the regular cover (path to cover), there may also be a thumbnail attribute, which should be used in preference. The thumbnail attribute is of the form (width, height, cover_data as jpeg). :return: A list of 3-element tuples. The list is meant to be passed to :meth:`add_books_to_metadata`. ''' raise NotImplementedError()

[documentos] @classmethod def add_books_to_metadata(cls, locations, metadata, booklists): ''' Add locations to the booklists. This function must not communicate with the device. :param locations: Result of a call to L{upload_books} :param metadata: List of :class:`Metadata` objects, same as for :meth:`upload_books`. :param booklists: A tuple containing the result of calls to (:meth:`books(oncard=None)`, :meth:`books(oncard='carda')`, :meth`books(oncard='cardb')`). ''' raise NotImplementedError()

[documentos] def delete_books(self, paths, end_session=True): ''' Delete books at paths on device. ''' raise NotImplementedError()

[documentos] @classmethod def remove_books_from_metadata(cls, paths, booklists): ''' Remove books from the metadata list. This function must not communicate with the device. :param paths: paths to books on the device. :param booklists: A tuple containing the result of calls to (:meth:`books(oncard=None)`, :meth:`books(oncard='carda')`, :meth`books(oncard='cardb')`). ''' raise NotImplementedError()

[documentos] def sync_booklists(self, booklists, end_session=True): ''' Update metadata on device. :param booklists: A tuple containing the result of calls to (:meth:`books(oncard=None)`, :meth:`books(oncard='carda')`, :meth`books(oncard='cardb')`). ''' raise NotImplementedError()

[documentos] def get_file(self, path, outfile, end_session=True): ''' Read the file at ``path`` on the device and write it to outfile. :param outfile: file object like ``sys.stdout`` or the result of an :func:`open` call. ''' raise NotImplementedError()

[documentos] @classmethod def config_widget(cls): ''' Should return a QWidget. The QWidget contains the settings for the device interface ''' raise NotImplementedError()

[documentos] @classmethod def save_settings(cls, settings_widget): ''' Should save settings to disk. Takes the widget created in :meth:`config_widget` and saves all settings to disk. ''' raise NotImplementedError()

[documentos] @classmethod def settings(cls): ''' Should return an opts object. The opts object should have at least one attribute `format_map` which is an ordered list of formats for the device. ''' raise NotImplementedError()

[documentos] def set_plugboards(self, plugboards, pb_func): ''' provide the driver the current set of plugboards and a function to select a specific plugboard. This method is called immediately before add_books and sync_booklists. pb_func is a callable with the following signature:: def pb_func(device_name, format, plugboards) You give it the current device name (either the class name or DEVICE_PLUGBOARD_NAME), the format you are interested in (a 'real' format or 'device_db'), and the plugboards (you were given those by set_plugboards, the same place you got this method). :return: None or a single plugboard instance. ''' pass

[documentos] def set_driveinfo_name(self, location_code, name): ''' Set the device name in the driveinfo file to 'name'. This setting will persist until the file is re-created or the name is changed again. Non-disk devices should implement this method based on the location codes returned by the get_device_information() method. ''' pass

[documentos] def prepare_addable_books(self, paths): ''' Given a list of paths, returns another list of paths. These paths point to addable versions of the books. If there is an error preparing a book, then instead of a path, the position in the returned list for that book should be a three tuple: (original_path, the exception instance, traceback) ''' return paths

[documentos] def startup(self): ''' Called when calibre is starting the device. Do any initialization required. Note that multiple instances of the class can be instantiated, and thus __init__ can be called multiple times, but only one instance will have this method called. This method is called on the device thread, not the GUI thread. ''' pass

[documentos] def shutdown(self): ''' Called when calibre is shutting down, either for good or in preparation to restart. Do any cleanup required. This method is called on the device thread, not the GUI thread. ''' pass

[documentos] def get_device_uid(self): ''' Must return a unique id for the currently connected device (this is called immediately after a successful call to open()). You must implement this method if you set ASK_TO_ALLOW_CONNECT = True ''' raise NotImplementedError()

[documentos] def ignore_connected_device(self, uid): ''' Should ignore the device identified by uid (the result of a call to get_device_uid()) in the future. You must implement this method if you set ASK_TO_ALLOW_CONNECT = True. Note that this function is called immediately after open(), so if open() caches some state, the driver should reset that state. ''' raise NotImplementedError()

[documentos] def get_user_blacklisted_devices(self): ''' Return map of device uid to friendly name for all devices that the user has asked to be ignored. ''' return {}

[documentos] def set_user_blacklisted_devices(self, devices): ''' Set the list of device uids that should be ignored by this driver. ''' pass

[documentos] def specialize_global_preferences(self, device_prefs): ''' Implement this method if your device wants to override a particular preference. You must ensure that all call sites that want a preference that can be overridden use device_prefs['something'] instead of prefs['something']. Your method should call device_prefs.set_overrides(pref=val, pref=val, ...). Currently used for: metadata management (prefs['manage_device_metadata']) ''' device_prefs.set_overrides()

[documentos] def set_library_info(self, library_name, library_uuid, field_metadata): ''' Implement this method if you want information about the current calibre library. This method is called at startup and when the calibre library changes while connected. ''' pass

# Dynamic control interface. # The following methods are probably called on the GUI thread. Any driver # that implements these methods must take pains to be thread safe, because # the device_manager might be using the driver at the same time that one of # these methods is called.

[documentos] def is_dynamically_controllable(self): ''' Called by the device manager when starting plugins. If this method returns a string, then a) it supports the device manager's dynamic control interface, and b) that name is to be used when talking to the plugin. This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' return None

[documentos] def start_plugin(self): ''' This method is called to start the plugin. The plugin should begin to accept device connections however it does that. If the plugin is already accepting connections, then do nothing. This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' pass

[documentos] def stop_plugin(self): ''' This method is called to stop the plugin. The plugin should no longer accept connections, and should cleanup behind itself. It is likely that this method should call shutdown. If the plugin is already not accepting connections, then do nothing. This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' pass

[documentos] def get_option(self, opt_string, default=None): ''' Return the value of the option indicated by opt_string. This method can be called when the plugin is not started. Return None if the option does not exist. This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' return default

[documentos] def set_option(self, opt_string, opt_value): ''' Set the value of the option indicated by opt_string. This method can be called when the plugin is not started. This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' pass

[documentos] def is_running(self): ''' Return True if the plugin is started, otherwise false This method can be called on the GUI thread. A driver that implements this method must be thread safe. ''' return False

[documentos] def synchronize_with_db(self, db, book_id, book_metadata, first_call): ''' Called during book matching when a book on the device is matched with a book in calibre's db. The method is responsible for synchronizing data from the device to calibre's db (if needed). The method must return a two-value tuple. The first value is a set of calibre book ids changed if calibre's database was changed or None if the database was not changed. If the first value is an empty set then the metadata for the book on the device is updated with calibre's metadata and given back to the device, but no GUI refresh of that book is done. This is useful when the calibre data is correct but must be sent to the device. The second value is itself a 2-value tuple. The first value in the tuple specifies whether a book format should be sent to the device. The intent is to permit verifying that the book on the device is the same as the book in calibre. This value must be None if no book is to be sent, otherwise return the base file name on the device (a string like foobar.epub). Be sure to include the extension in the name. The device subsystem will construct a send_books job for all books with not- None returned values. Note: other than to later retrieve the extension, the name is ignored in cases where the device uses a template to generate the file name, which most do. The second value in the returned tuple indicated whether the format is future-dated. Return True if it is, otherwise return False. calibre will display a dialog to the user listing all future dated books. Extremely important: this method is called on the GUI thread. It must be threadsafe with respect to the device manager's thread. book_id: the calibre id for the book in the database. book_metadata: the Metadata object for the book coming from the device. first_call: True if this is the first call during a sync, False otherwise ''' return (None, (None, False))

[documentos] class BookList(list): ''' A list of books. Each Book object must have the fields #. title #. authors #. size (file size of the book) #. datetime (a UTC time tuple) #. path (path on the device to the book) #. thumbnail (can be None) thumbnail is either a str/bytes object with the image data or it should have an attribute image_path that stores an absolute (platform native) path to the image #. tags (a list of strings, can be empty). ''' __getslice__ = None __setslice__ = None def __init__(self, oncard, prefix, settings): pass

[documentos] def supports_collections(self): ''' Return True if the device supports collections for this book list. ''' raise NotImplementedError()

[documentos] def add_book(self, book, replace_metadata): ''' Add the book to the booklist. Intent is to maintain any device-internal metadata. Return True if booklists must be sync'ed ''' raise NotImplementedError()

[documentos] def remove_book(self, book): ''' Remove a book from the booklist. Correct any device metadata at the same time ''' raise NotImplementedError()

[documentos] def get_collections(self, collection_attributes): ''' Return a dictionary of collections created from collection_attributes. Each entry in the dictionary is of the form collection name:[list of books] The list of books is sorted by book title, except for collections created from series, in which case series_index is used. :param collection_attributes: A list of attributes of the Book object ''' raise NotImplementedError()

class CurrentlyConnectedDevice:

    def __init__(self):
        self._device = None

    @property
    def device(self):
        return self._device

# A device driver can check if a device is currently connected to calibre using
# the following code::
#   from calibre.device.interface import currently_connected_device
#   if currently_connected_device.device is None:
#      # no device connected
# The device attribute will be either None or the device driver object
# (DevicePlugin instance) for the currently connected device.
currently_connected_device = CurrentlyConnectedDevice()

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.devices.usbms.cli

Código fuente para calibre.devices.usbms.cli

__license__   = 'GPL v3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'

import os
import shutil
import time

from calibre import fsync
from calibre.devices.errors import PathError
from calibre.utils.filenames import case_preserving_open_file

class File:

    def __init__(self, path):
        stats = os.stat(path)
        self.is_dir = os.path.isdir(path)
        self.is_readonly = not os.access(path, os.W_OK)
        self.ctime = stats.st_ctime
        self.wtime = stats.st_mtime
        self.size  = stats.st_size
        if path.endswith(os.sep):
            path = path[:-1]
        self.path = path
        self.name = os.path.basename(path)

def check_transfer(infile, dest):
    infile.seek(0)
    dest.seek(0)
    return infile.read() == dest.read()

[documentos] class CLI: def get_file(self, path, outfile, end_session=True): path = self.munge_path(path) with open(path, 'rb') as src: shutil.copyfileobj(src, outfile) def put_file(self, infile, path, replace_file=False, end_session=True): path = self.munge_path(path) close = False if not hasattr(infile, 'read'): infile, close = open(infile, 'rb'), True infile.seek(0) if os.path.isdir(path): path = os.path.join(path, infile.name) if not replace_file and os.path.exists(path): raise PathError('File already exists: ' + path) dest, actual_path = case_preserving_open_file(path) with dest: try: shutil.copyfileobj(infile, dest) except OSError: print('WARNING: First attempt to send file to device failed') time.sleep(0.2) infile.seek(0) dest.seek(0) dest.truncate() shutil.copyfileobj(infile, dest) fsync(dest) # if not check_transfer(infile, dest): raise Exception('Transfer failed') if close: infile.close() return actual_path def munge_path(self, path): if path.startswith('/') and not (path.startswith(self._main_prefix) or (self._card_a_prefix and path.startswith(self._card_a_prefix)) or (self._card_b_prefix and path.startswith(self._card_b_prefix))): path = self._main_prefix + path[1:] elif path.startswith('carda:'): path = path.replace('carda:', self._card_a_prefix[:-1]) elif path.startswith('cardb:'): path = path.replace('cardb:', self._card_b_prefix[:-1]) return path def list(self, path, recurse=False, end_session=True, munge=True): if munge: path = self.munge_path(path) if os.path.isfile(path): return [(os.path.dirname(path), [File(path)])] entries = [File(os.path.join(path, f)) for f in os.listdir(path)] dirs = [(path, entries)] for _file in entries: if recurse and _file.is_dir: dirs[len(dirs):] = self.list(_file.path, recurse=True, munge=False) return dirs def mkdir(self, path, end_session=True): if self.SUPPORTS_SUB_DIRS: path = self.munge_path(path) os.mkdir(path) def rm(self, path, end_session=True): path = self.munge_path(path) self.delete_books([path]) def touch(self, path, end_session=True): path = self.munge_path(path) if not os.path.exists(path): open(path, 'wb').close() if not os.path.isdir(path): os.utime(path, None)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.devices.usbms.device

Código fuente para calibre.devices.usbms.device

__license__   = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com> ' \
                '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

'''
Generic device driver. This is not a complete stand alone driver. It is
intended to be subclassed with the relevant parts implemented for a particular
device. This class handles device detection.
'''

import glob
import os
import re
import subprocess
import sys
import time
from collections import namedtuple
from itertools import repeat

from calibre import prints
from calibre.constants import is_debugging, isfreebsd, islinux, ismacos, iswindows
from calibre.devices.errors import DeviceError
from calibre.devices.interface import DevicePlugin
from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre.utils.filenames import ascii_filename as sanitize
from polyglot.builtins import iteritems, string_or_bytes

if ismacos:
    osx_sanitize_name_pat = re.compile(r'[.-]')

if iswindows:
    usb_info_cache = {}

def eject_exe():
    base = sys.extensions_location if hasattr(sys, 'new_app_layout') else os.path.dirname(sys.executable)
    return os.path.join(base, 'calibre-eject.exe')

class USBDevice:

    def __init__(self, dev):
        self.idVendor = dev[0]
        self.idProduct = dev[1]
        self.bcdDevice = dev[2]
        if iswindows:
            # Getting this information requires communicating with the device
            # we only do that in the can_handle_windows() method, if needed.
            self.manufacturer = self.serial = self.product = None
        else:
            self.manufacturer = dev[3]
            self.product = dev[4]
            self.serial = dev[5]

    def match_serial(self, serial):
        return self.serial and self.serial == serial

    def match_numbers(self, vid, pid, bcd):
        return self.idVendor == vid and self.idProduct == pid and self.bcdDevice == bcd

    def match_strings(self, vid, pid, bcd, man, prod):
        if not self.match_numbers(vid, pid, bcd):
            return False
        if man == self.manufacturer and prod == self.product:
            return True
        # As of macOS 10.11.4 Apple started mangling the names returned via the
        # IOKit registry. See
        # https://www.mobileread.com/forums/showthread.php?t=273213
        m = osx_sanitize_name_pat.sub('_', (self.manufacturer or ''))
        p = osx_sanitize_name_pat.sub('_', (self.product or ''))
        return m == man and p == prod

[documentos] class Device(DeviceConfig, DevicePlugin): ''' This class provides logic common to all drivers for devices that export themselves as USB Mass Storage devices. Provides implementations for mounting/ejecting of USBMS devices on all platforms. ''' VENDOR_ID = 0x0 PRODUCT_ID = 0x0 BCD = None VENDOR_NAME = None #: String identifying the main memory of the device in the Windows PnP id #: strings #: This can be None, string, list of strings or compiled regex WINDOWS_MAIN_MEM = None #: String identifying the first card of the device in the Windows PnP id #: strings #: This can be None, string, list of strings or compiled regex WINDOWS_CARD_A_MEM = None #: String identifying the second card of the device in the Windows PnP id #: strings #: This can be None, string, list of strings or compiled regex WINDOWS_CARD_B_MEM = None #: Used by the new driver detection to disambiguate main memory from #: storage cards. Should be a regular expression that matches the #: main memory mount point assigned by macOS OSX_MAIN_MEM_VOL_PAT = None OSX_EJECT_COMMAND = ['diskutil', 'eject'] MAIN_MEMORY_VOLUME_LABEL = '' STORAGE_CARD_VOLUME_LABEL = '' STORAGE_CARD2_VOLUME_LABEL = None EBOOK_DIR_MAIN = '' EBOOK_DIR_CARD_A = '' EBOOK_DIR_CARD_B = '' DELETE_EXTS = [] # USB disk-based devices can see the book files on the device, so can # copy these back to the library BACKLOADING_ERROR_MESSAGE = None #: The maximum length of paths created on the device MAX_PATH_LEN = 250 #: Put news in its own folder NEWS_IN_FOLDER = True

[documentos] def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.detected_device = None if detected_device is None else USBDevice(detected_device) self.set_progress_reporter(report_progress)

[documentos] def set_progress_reporter(self, report_progress): self.report_progress = report_progress self.report_progress = report_progress if self.report_progress is None: self.report_progress = lambda x, y: x

[documentos] def card_prefix(self, end_session=True): return (self._card_a_prefix, self._card_b_prefix)

@classmethod def _windows_space(cls, prefix): if not prefix: return 0, 0 prefix = prefix[:-1] from calibre_extensions import winutil try: available_space, total_space, free_space = winutil.get_disk_free_space(prefix) except OSError as err: if err.winerror == winutil.ERROR_NOT_READY: # Disk not ready time.sleep(3) available_space, total_space, free_space = winutil.get_disk_free_space(prefix) else: raise return total_space, available_space

[documentos] def total_space(self, end_session=True): msz = casz = cbsz = 0 if not iswindows: if self._main_prefix is not None: stats = os.statvfs(self._main_prefix) msz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) if self._card_a_prefix is not None: stats = os.statvfs(self._card_a_prefix) casz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) if self._card_b_prefix is not None: stats = os.statvfs(self._card_b_prefix) cbsz = stats.f_frsize * (stats.f_blocks + stats.f_bavail - stats.f_bfree) else: msz = self._windows_space(self._main_prefix)[0] casz = self._windows_space(self._card_a_prefix)[0] cbsz = self._windows_space(self._card_b_prefix)[0] return (msz, casz, cbsz)

[documentos] def free_space(self, end_session=True): msz = casz = cbsz = 0 if not iswindows: if self._main_prefix is not None: stats = os.statvfs(self._main_prefix) msz = stats.f_frsize * stats.f_bavail if self._card_a_prefix is not None: stats = os.statvfs(self._card_a_prefix) casz = stats.f_frsize * stats.f_bavail if self._card_b_prefix is not None: stats = os.statvfs(self._card_b_prefix) cbsz = stats.f_frsize * stats.f_bavail else: msz = self._windows_space(self._main_prefix)[1] casz = self._windows_space(self._card_a_prefix)[1] cbsz = self._windows_space(self._card_b_prefix)[1] return (msz, casz, cbsz)

def windows_filter_pnp_id(self, pnp_id): return False

[documentos] def windows_sort_drives(self, drives): ''' Called to disambiguate main memory and storage card for devices that do not distinguish between them on the basis of `WINDOWS_CARD_NAME`. For example: The EB600 ''' return drives

[documentos] def can_handle_windows(self, usbdevice, debug=False): if hasattr(self.can_handle, 'is_base_class_implementation'): # No custom can_handle implementation return True # Delegate to the unix can_handle function, creating a unix like # USBDevice object from calibre.devices.winusb import get_usb_info dev = usb_info_cache.get(usbdevice) if dev is None: try: data = get_usb_info(usbdevice, debug=debug) except Exception: time.sleep(0.1) try: data = get_usb_info(usbdevice, debug=debug) except Exception: data = {} dev = usb_info_cache[usbdevice] = namedtuple( 'USBDevice', 'vendor_id product_id bcd manufacturer product serial')( usbdevice.vendor_id, usbdevice.product_id, usbdevice.bcd, data.get('manufacturer') or '', data.get('product') or '', data.get('serial_number') or '') if debug: prints(f'USB Info for device: {dev}') return self.can_handle(dev, debug=debug)

def open_windows(self): from calibre.devices.scanner import drive_is_ok from calibre.devices.winusb import get_drive_letters_for_device usbdev = self.device_being_opened debug = is_debugging() or getattr(self, 'do_device_debug', False) try: dlmap = get_drive_letters_for_device(usbdev, debug=debug) except Exception: dlmap = {} if not dlmap.get('drive_letters'): time.sleep(7) dlmap = get_drive_letters_for_device(usbdev, debug=debug) if debug: from pprint import pformat prints(f'Drive letters for {usbdev}') prints(pformat(dlmap)) filtered = set() for dl in dlmap['drive_letters']: pnp_id = dlmap['pnp_id_map'][dl].upper() if dl in dlmap['readonly_drives']: filtered.add(dl) if debug: prints('Ignoring the drive %s as it is readonly' % dl) elif self.windows_filter_pnp_id(pnp_id): filtered.add(dl) if debug: prints(f'Ignoring the drive {dl} because of a PNP filter on {pnp_id}') elif not drive_is_ok(dl, debug=debug): filtered.add(dl) if debug: prints('Ignoring the drive %s because failed to get free space for it' % dl) dlmap['drive_letters'] = [dl for dl in dlmap['drive_letters'] if dl not in filtered] if not dlmap['drive_letters']: raise DeviceError(_('Unable to detect any disk drives for the device: %s. Try rebooting') % self.get_gui_name()) drives = {} for drive_letter, which in zip(dlmap['drive_letters'], 'main carda cardb'.split()): drives[which] = drive_letter + ':\\' drives = self.windows_sort_drives(drives) self._main_prefix = drives.get('main') self._card_a_prefix = drives.get('carda', None) self._card_b_prefix = drives.get('cardb', None) @classmethod def run_ioreg(cls, raw=None): if raw is not None: return raw ioreg = '/usr/sbin/ioreg' if not os.access(ioreg, os.X_OK): ioreg = 'ioreg' cmd = (ioreg+' -w 0 -S -c IOMedia').split() for i in range(3): try: return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] except OSError: # Probably an interrupted system call if i == 2: raise time.sleep(2) def osx_sort_names(self, names): return names @classmethod def osx_run_mount(cls): for i in range(3): try: return subprocess.Popen('mount', stdout=subprocess.PIPE).communicate()[0] except OSError: # Probably an interrupted system call if i == 2: raise time.sleep(2) @classmethod def osx_get_usb_drives(cls): from calibre_extensions.usbobserver import get_usb_drives return get_usb_drives() def _osx_bsd_names(self): drives = self.osx_get_usb_drives() matches = [] d = self.detected_device if d.serial: for path, vid, pid, bcd, ven, prod, serial in drives: if d.match_serial(serial): matches.append(path) if not matches and d.manufacturer and d.product: for path, vid, pid, bcd, man, prod, serial in drives: if d.match_strings(vid, pid, bcd, man, prod): matches.append(path) if not matches: # Since Apple started mangling the names stored in the IOKit # registry, we cannot trust match_strings() so fallback to matching # on just numbers. See http://www.mobileread.com/forums/showthread.php?t=273213 for path, vid, pid, bcd, man, prod, serial in drives: if d.match_numbers(vid, pid, bcd): matches.append(path) if not matches: from pprint import pformat raise DeviceError( f'Could not detect BSD names for {self.name}. Try rebooting.\nOutput from osx_get_usb_drives():\n{pformat(drives)}') pat = re.compile(r'(?P<m>\d+)([a-z]+(?P<p>\d+)){0,1}') def nums(x): 'Return (disk num, partition number)' m = pat.search(x) if m is None: return (10000, -1) g = m.groupdict() if g['p'] is None: g['p'] = 0 return list(map(int, (g.get('m'), g.get('p')))) def cmp_key(x): ''' Sorting based on the following scheme: - disks without partitions are first - sub sorted based on disk number - disks with partitions are sorted first on disk number, then on partition number ''' x = x.rpartition('/')[-1] disk_num, part_num = nums(x) has_part = 1 if part_num > 0 else 0 return has_part, disk_num, part_num matches.sort(key=cmp_key) drives = {'main':matches[0]} if len(matches) > 1: drives['carda'] = matches[1] if len(matches) > 2: drives['cardb'] = matches[2] return drives def osx_bsd_names(self): drives = {} for i in range(3): try: drives = self._osx_bsd_names() if len(drives) > 1: # wait for device to settle and SD card (if any) to become available return drives except Exception: if i == 2: raise time.sleep(3) return drives def open_osx(self): from calibre_extensions.usbobserver import get_mounted_filesystems bsd_drives = self.osx_bsd_names() drives = self.osx_sort_names(bsd_drives.copy()) mount_map = get_mounted_filesystems() # macOS 13 Ventura uses a weird scheme for mounted FAT devices of the # form fat://basename_of_bsd_name/basename_of_mountpoint # see https://www.mobileread.com/forums/showthread.php?t=347294 for dev_node in tuple(mount_map): if ':' in dev_node and '//' in dev_node: val = mount_map[dev_node] dev_node = dev_node.split('/')[-2] dev_node = f'/dev/{dev_node}' if dev_node not in mount_map: mount_map[dev_node] = val drives = {k: mount_map.get(v) for k, v in iteritems(drives)} if is_debugging(): print() from pprint import pprint pprint({'bsd_drives': bsd_drives, 'mount_map': mount_map, 'drives': drives}) if drives.get('carda') is None and drives.get('cardb') is not None: drives['carda'] = drives.pop('cardb') if drives.get('main') is None and drives.get('carda') is not None: drives['main'] = drives.pop('carda') if drives.get('carda') is None and drives.get('cardb') is not None: drives['carda'] = drives.pop('cardb') if drives.get('main') is None: raise DeviceError(_('Unable to detect the %s mount point. Try rebooting.')%self.__class__.__name__) pat = self.OSX_MAIN_MEM_VOL_PAT if pat is not None and len(drives) > 1 and 'main' in drives: if pat.search(drives['main']) is None: main = drives['main'] for x in ('carda', 'cardb'): if x in drives and pat.search(drives[x]): drives['main'] = drives.pop(x) drives[x] = main break self._main_prefix = drives['main']+os.sep def get_card_prefix(c): ans = drives.get(c, None) if ans is not None: ans += os.sep return ans self._card_a_prefix = get_card_prefix('carda') self._card_b_prefix = get_card_prefix('cardb') def find_device_nodes(self, detected_device=None): def walk(base): base = os.path.abspath(os.path.realpath(base)) for x in os.listdir(base): p = os.path.join(base, x) if os.path.islink(p) or not os.access(p, os.R_OK): continue isfile = os.path.isfile(p) yield p, isfile if not isfile: yield from walk(p) def raw2num(raw): raw = raw.lower() if not raw.startswith('0x'): raw = '0x' + raw return int(raw, 16) # Find device node based on vendor, product and bcd d, j = os.path.dirname, os.path.join usb_dir = None if detected_device is None: detected_device = self.detected_device def test(val, attr): q = getattr(detected_device, attr) return q == val def getnum(usb_dir): def rc(q): with open(j(usb_dir, q), 'rb') as f: return raw2num(f.read().decode('utf-8')) return rc for x, isfile in walk('/sys/devices'): if isfile and x.endswith('idVendor'): usb_dir = d(x) for y in ('idProduct', 'idVendor', 'bcdDevice'): if not os.access(j(usb_dir, y), os.R_OK): usb_dir = None break if usb_dir is None: continue ven, prod, bcd = map(getnum(usb_dir), ('idVendor', 'idProduct', 'bcdDevice')) if not (test(ven, 'idVendor') and test(prod, 'idProduct') and test(bcd, 'bcdDevice')): usb_dir = None continue else: break if usb_dir is None: raise DeviceError(_('Unable to detect the %s disk drive.') %self.__class__.__name__) devnodes, ok = [], {} for x, isfile in walk(usb_dir): if not isfile and '/block/' in x: parts = x.split('/') idx = parts.index('block') if idx == len(parts)-2: sz = j(x, 'size') node = parts[idx+1] try: with open(sz, 'rb') as szf: exists = int(szf.read().decode('utf-8')) > 0 if exists: node = self.find_largest_partition(x) ok[node] = True else: ok[node] = False except: ok[node] = False if is_debugging() and not ok[node]: print(f'\nIgnoring the node: {node} as could not read size from: {sz}') devnodes.append(node) devnodes += list(repeat(None, 3)) ans = ['/dev/'+x if ok.get(x, False) else None for x in devnodes] ans.sort(key=lambda x: x[5:] if x else 'zzzzz') return self.linux_swap_drives(ans[:3]) def linux_swap_drives(self, drives): return drives def node_mountpoint(self, node): from calibre.devices.udisks import node_mountpoint return node_mountpoint(node) def find_largest_partition(self, path): node = path.split('/')[-1] nodes = [] for x in glob.glob(path+'/'+node+'*'): sz = x + '/size' if not os.access(sz, os.R_OK): continue try: with open(sz, 'rb') as szf: sz = int(szf.read().decode('utf-8')) except: continue if sz > 0: nodes.append((x.split('/')[-1], sz)) nodes.sort(key=lambda x: x[1]) if not nodes: return node return nodes[-1][0] def open_linux(self): def mount(node, type): mp = self.node_mountpoint(node) if mp is not None: return mp, 0 def do_mount(node): try: from calibre.devices.udisks import mount mount(node) return 0 except: print('Udisks mount call failed:') import traceback traceback.print_exc() return 1 ret = do_mount(node) if ret != 0: return None, ret return self.node_mountpoint(node)+'/', 0 main, carda, cardb = self.find_device_nodes() if main is None: raise DeviceError(_('Unable to detect the %s disk drive. Either ' 'the device has already been ejected, or your ' 'kernel is exporting a deprecated version of SYSFS.') %self.__class__.__name__) if is_debugging(): print('\nFound device nodes:', main, carda, cardb) self._linux_mount_map = {} mp, ret = mount(main, 'main') if mp is None: raise DeviceError( _('Unable to mount main memory (Error code: %d)')%ret) if not mp.endswith('/'): mp += '/' self._linux_mount_map[main] = mp self._main_prefix = mp self._linux_main_device_node = main cards = [(carda, '_card_a_prefix', 'carda'), (cardb, '_card_b_prefix', 'cardb')] for card, prefix, typ in cards: if card is None: continue mp, ret = mount(card, typ) if mp is None: print('Unable to mount card (Error code: %d)'%ret, file=sys.stderr) else: if not mp.endswith('/'): mp += '/' setattr(self, prefix, mp) self._linux_mount_map[card] = mp self.filter_read_only_mount_points() def filter_read_only_mount_points(self): def is_readonly(mp): if mp is None: return True path = os.path.join(mp, 'calibre_readonly_test') ro = True try: with open(path, 'wb'): ro = False except: pass else: try: os.remove(path) except: pass if is_debugging() and ro: print('\nThe mountpoint', mp, 'is readonly, ignoring it') return ro for mp in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'): if is_readonly(getattr(self, mp, None)): setattr(self, mp, None) if self._main_prefix is None: for p in ('_card_a_prefix', '_card_b_prefix'): nmp = getattr(self, p, None) if nmp is not None: self._main_prefix = nmp setattr(self, p, None) break if self._main_prefix is None: raise DeviceError(_('The main memory of %s is read only. ' 'This usually happens because of file system errors.') %self.__class__.__name__) if self._card_a_prefix is None and self._card_b_prefix is not None: self._card_a_prefix = self._card_b_prefix self._card_b_prefix = None # ------------------------------------------------------ # # open for FreeBSD # find the device node or nodes that match the S/N we already have from the scanner # and attempt to mount each one # 1. get list of devices in /dev with matching s/n etc. # 2. get list of volumes associated with each # 3. attempt to mount each one using Hal # 4. when finished, we have a list of mount points and associated dbus nodes # def open_freebsd(self): # There should be some way to access the -v arg... verbose = False # this gives us access to the S/N, etc. of the reader that the scanner has found # and the match routines for some of that data, like s/n, vendor ID, etc. d=self.detected_device if not d.serial: raise DeviceError("Device has no S/N. Can't continue") from .hal import get_hal hal = get_hal() vols = hal.get_volumes(d) if verbose: print("FBSD: ", vols) ok, mv = hal.mount_volumes(vols) if not ok: raise DeviceError(_('Unable to mount the device')) for k, v in mv.items(): setattr(self, k, v) # # ------------------------------------------------------ # # this one is pretty simple: # just umount each of the previously # mounted filesystems, using the stored volume object # def eject_freebsd(self): from .hal import get_hal hal = get_hal() if self._main_prefix: hal.unmount(self._main_vol) if self._card_a_prefix: hal.unmount(self._card_a_vol) if self._card_b_prefix: hal.unmount(self._card_b_vol) self._main_prefix = self._main_vol = None self._card_a_prefix = self._card_a_vol = None self._card_b_prefix = self._card_b_vol = None # ------------------------------------------------------

[documentos] def open(self, connected_device, library_uuid): time.sleep(5) self._main_prefix = self._card_a_prefix = self._card_b_prefix = None self.device_being_opened = connected_device try: if islinux: try: self.open_linux() except DeviceError: time.sleep(7) self.open_linux() if isfreebsd: self._main_vol = self._card_a_vol = self._card_b_vol = None try: self.open_freebsd() except DeviceError: time.sleep(2) self.open_freebsd() if iswindows: self.open_windows() if ismacos: try: self.open_osx() except DeviceError: time.sleep(7) self.open_osx() self.current_library_uuid = library_uuid self.post_open_callback() finally: self.device_being_opened = None

def post_open_callback(self): pass def eject_windows(self): from threading import Thread drives = [] for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'): x = getattr(self, x, None) if x is not None: drives.append(x[0].upper()) def do_it(drives): subprocess.Popen([eject_exe()] + drives, creationflags=subprocess.CREATE_NO_WINDOW).wait() t = Thread(target=do_it, args=[drives]) t.daemon = True t.start() self.__save_win_eject_thread = t def eject_osx(self): for x in ('_main_prefix', '_card_a_prefix', '_card_b_prefix'): x = getattr(self, x, None) if x is not None: try: subprocess.Popen(self.OSX_EJECT_COMMAND + [x]) except: pass def eject_linux(self): from calibre.devices.udisks import eject, umount drives = [d for d in self.find_device_nodes() if d] for d in drives: try: umount(d) except: pass for d in drives: try: eject(d) except Exception as e: print('Udisks eject call for:', d, 'failed:') print('\t', e)

[documentos] def eject(self): if islinux: try: self.eject_linux() except: pass if isfreebsd: try: self.eject_freebsd() except: pass if iswindows: try: self.eject_windows() except: pass if ismacos: try: self.eject_osx() except: pass self._main_prefix = self._card_a_prefix = self._card_b_prefix = None

def linux_post_yank(self): self._linux_mount_map = {}

[documentos] def post_yank_cleanup(self): if islinux: try: self.linux_post_yank() except: import traceback traceback.print_exc() self._main_prefix = self._card_a_prefix = self._card_b_prefix = None

def get_main_ebook_dir(self, for_upload=False): return self.EBOOK_DIR_MAIN def get_carda_ebook_dir(self, for_upload=False): return self.EBOOK_DIR_CARD_A def get_cardb_ebook_dir(self, for_upload=False): return self.EBOOK_DIR_CARD_B def _sanity_check(self, on_card, files): from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space()) def get_dest_dir(prefix, candidates): if isinstance(candidates, string_or_bytes): candidates = [candidates] if not candidates: candidates = [''] candidates = [ ((os.path.join(prefix, *(x.split('/')))) if x else prefix) for x in candidates] existing = [x for x in candidates if os.path.exists(x)] if not existing: existing = candidates return existing[0] if on_card == 'carda': candidates = self.get_carda_ebook_dir(for_upload=True) path = get_dest_dir(self._card_a_prefix, candidates) elif on_card == 'cardb': candidates = self.get_cardb_ebook_dir(for_upload=True) path = get_dest_dir(self._card_b_prefix, candidates) else: candidates = self.get_main_ebook_dir(for_upload=True) path = get_dest_dir(self._main_prefix, candidates) return path

[documentos] def sanitize_callback(self, path): ''' Callback to allow individual device drivers to override the path sanitization used by :meth:`create_upload_path`. ''' return sanitize(path)

[documentos] def filename_callback(self, default, mi): ''' Callback to allow drivers to change the default file name set by :meth:`create_upload_path`. ''' return default

[documentos] def sanitize_path_components(self, components): ''' Perform any device specific sanitization on the path components for files to be uploaded to the device ''' return components

[documentos] def get_annotations(self, path_map): ''' Resolve path_map to annotation_map of files found on the device ''' return {}

[documentos] def add_annotation_to_library(self, db, db_id, annotation): ''' Add an annotation to the calibre library ''' pass

def create_upload_path(self, path, mdata, fname, create_dirs=True): from calibre.devices.utils import create_upload_path settings = self.settings() filepath = create_upload_path(mdata, fname, self.save_template(), self.sanitize_callback, prefix_path=os.path.abspath(path), maxlen=self.MAX_PATH_LEN, use_subdirs=self.SUPPORTS_SUB_DIRS and settings.use_subdirs, news_in_folder=self.NEWS_IN_FOLDER, filename_callback=self.filename_callback, sanitize_path_components=self.sanitize_path_components ) filedir = os.path.dirname(filepath) if create_dirs and not os.path.exists(filedir): os.makedirs(filedir) return filepath def create_annotations_path(self, mdata, device_path=None): return self.create_upload_path(os.path.abspath('/<storage>'), mdata, 'x.bookmark', create_dirs=False)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.devices.usbms.driver

Código fuente para calibre.devices.usbms.driver

__license__   = 'GPL v3'
__copyright__ = '2009, John Schember <john at nachtimwald.com>'
__docformat__ = 'restructuredtext en'

'''
Generic USB Mass storage device driver. This is not a complete stand alone
driver. It is intended to be subclassed with the relevant parts implemented
for a particular device.
'''

import json
import os
import shutil
import time
from itertools import cycle

from calibre import fsync, isbytestring, prints
from calibre.constants import filesystem_encoding, is_debugging, ismacos, numeric_version
from calibre.devices.usbms.books import Book, BookList
from calibre.devices.usbms.cli import CLI
from calibre.devices.usbms.device import Device
from calibre.ebooks.metadata.book.json_codec import JsonCodec
from polyglot.builtins import itervalues, string_or_bytes

def debug_print(*args, **kw):
    base_time = getattr(debug_print, 'base_time', None)
    if base_time is None:
        debug_print.base_time = base_time = time.monotonic()
    if is_debugging():
        prints('DEBUG: %6.1f'%(time.monotonic()-base_time), *args, **kw)

def safe_walk(top, topdown=True, onerror=None, followlinks=False, maxdepth=128):
    ' A replacement for os.walk that does not die when it encounters undecodeable filenames in a linux filesystem'
    if maxdepth < 0:
        return
    islink, join, isdir = os.path.islink, os.path.join, os.path.isdir

    # We may not have read permission for top, in which case we can't
    # get a list of the files the directory contains.  os.path.walk
    # always suppressed the exception then, rather than blow up for a
    # minor reason when (say) a thousand readable directories are still
    # left to visit.  That logic is copied here.
    try:
        names = os.listdir(top)
    except OSError as err:
        if onerror is not None:
            onerror(err)
        return

    dirs, nondirs = [], []
    for name in names:
        if isinstance(name, bytes):
            try:
                name = name.decode(filesystem_encoding)
            except UnicodeDecodeError:
                debug_print('Skipping undecodeable file: %r' % name)
                continue
        if isdir(join(top, name)):
            dirs.append(name)
        else:
            nondirs.append(name)

    if topdown:
        yield top, dirs, nondirs
    for name in dirs:
        new_path = join(top, name)
        if followlinks or not islink(new_path):
            yield from safe_walk(new_path, topdown, onerror, followlinks, maxdepth-1)
    if not topdown:
        yield top, dirs, nondirs

# CLI must come before Device as it implements the CLI functions that
# are inherited from the device interface in Device.

[documentos] class USBMS(CLI, Device): ''' The base class for all USBMS devices. Implements the logic for sending/getting/updating metadata/caching metadata/etc. ''' description = _('Communicate with an e-book reader.') author = 'John Schember' supported_platforms = ['windows', 'osx', 'linux'] # Store type instances of BookList and Book. We must do this because # a) we need to override these classes in some device drivers, and # b) the classmethods seem only to see real attributes declared in the # class, not attributes stored in the class booklist_class = BookList book_class = Book FORMATS = [] CAN_SET_METADATA = [] METADATA_CACHE = 'metadata.calibre' DRIVEINFO = 'driveinfo.calibre' SCAN_FROM_ROOT = False def _update_driveinfo_record(self, dinfo, prefix, location_code, name=None): import uuid from calibre.utils.date import isoformat, now if not isinstance(dinfo, dict): dinfo = {} if dinfo.get('device_store_uuid', None) is None: dinfo['device_store_uuid'] = str(uuid.uuid4()) if dinfo.get('device_name', None) is None: dinfo['device_name'] = self.get_gui_name() if name is not None: dinfo['device_name'] = name dinfo['location_code'] = location_code dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None) dinfo['calibre_version'] = '.'.join([str(i) for i in numeric_version]) dinfo['date_last_connected'] = isoformat(now()) dinfo['prefix'] = prefix.replace('\\', '/') return dinfo def _update_driveinfo_file(self, prefix, location_code, name=None): from calibre.utils.config import from_json, to_json if os.path.exists(os.path.join(prefix, self.DRIVEINFO)): with open(os.path.join(prefix, self.DRIVEINFO), 'rb') as f: try: driveinfo = json.loads(f.read(), object_hook=from_json) except: driveinfo = None driveinfo = self._update_driveinfo_record(driveinfo, prefix, location_code, name) data = json.dumps(driveinfo, default=to_json) if not isinstance(data, bytes): data = data.encode('utf-8') with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f: f.write(data) fsync(f) else: driveinfo = self._update_driveinfo_record({}, prefix, location_code, name) data = json.dumps(driveinfo, default=to_json) if not isinstance(data, bytes): data = data.encode('utf-8') with open(os.path.join(prefix, self.DRIVEINFO), 'wb') as f: f.write(data) fsync(f) return driveinfo

[documentos] def get_device_information(self, end_session=True): self.report_progress(1.0, _('Get device information...')) self.driveinfo = {} def raise_os_error(e): raise OSError(_('Failed to access files in the main memory of' ' your device. You should contact the device' ' manufacturer for support. Common fixes are:' ' try a different USB cable/USB port on your computer.' ' If you device has a "Reset to factory defaults" type' ' of setting somewhere, use it. Underlying error: %s') % e) from e if self._main_prefix is not None: try: self.driveinfo['main'] = self._update_driveinfo_file(self._main_prefix, 'main') except PermissionError as e: if ismacos: raise PermissionError(_( 'Permission was denied by macOS trying to access files in the main memory of' ' your device. You will need to grant permission explicitly by looking under' ' System Preferences > Security and Privacy > Privacy > Files and Folders.' ' Underlying error: %s' ) % e) from e raise_os_error(e) except OSError as e: raise_os_error(e) try: if self._card_a_prefix is not None: self.driveinfo['A'] = self._update_driveinfo_file(self._card_a_prefix, 'A') if self._card_b_prefix is not None: self.driveinfo['B'] = self._update_driveinfo_file(self._card_b_prefix, 'B') except OSError as e: raise OSError(_('Failed to access files on the SD card in your' ' device. This can happen for many reasons. The SD card may be' ' corrupted, it may be too large for your device, it may be' ' write-protected, etc. Try a different SD card, or reformat' ' your SD card using the FAT32 filesystem. Also make sure' ' there are not too many files in the root of your SD card.' ' Underlying error: %s') % e) return (self.get_gui_name(), '', '', '', self.driveinfo)

[documentos] def set_driveinfo_name(self, location_code, name): if location_code == 'main': self._update_driveinfo_file(self._main_prefix, location_code, name) elif location_code == 'A': self._update_driveinfo_file(self._card_a_prefix, location_code, name) elif location_code == 'B': self._update_driveinfo_file(self._card_b_prefix, location_code, name)

def formats_to_scan_for(self): return set(self.settings().format_map) | set(self.FORMATS) def is_allowed_book_file(self, filename, path, prefix): return True

[documentos] def books(self, oncard=None, end_session=True): from calibre.ebooks.metadata.meta import path_to_ext debug_print('USBMS: Fetching list of books from device. Device=', self.__class__.__name__, 'oncard=', oncard) dummy_bl = self.booklist_class(None, None, None) if oncard == 'carda' and not self._card_a_prefix: self.report_progress(1.0, _('Getting list of books on device...')) return dummy_bl elif oncard == 'cardb' and not self._card_b_prefix: self.report_progress(1.0, _('Getting list of books on device...')) return dummy_bl elif oncard and oncard != 'carda' and oncard != 'cardb': self.report_progress(1.0, _('Getting list of books on device...')) return dummy_bl prefix = self._card_a_prefix if oncard == 'carda' else \ self._card_b_prefix if oncard == 'cardb' \ else self._main_prefix ebook_dirs = self.get_carda_ebook_dir() if oncard == 'carda' else \ self.EBOOK_DIR_CARD_B if oncard == 'cardb' else \ self.get_main_ebook_dir() debug_print('USBMS: dirs are:', prefix, ebook_dirs) # get the metadata cache bl = self.booklist_class(oncard, prefix, self.settings) need_sync = self.parse_metadata_cache(bl, prefix, self.METADATA_CACHE) # make a dict cache of paths so the lookup in the loop below is faster. bl_cache = {} for idx, b in enumerate(bl): bl_cache[b.lpath] = idx all_formats = self.formats_to_scan_for() def update_booklist(filename, path, prefix): changed = False # Ignore AppleDouble files if filename.startswith("._"): return False if path_to_ext(filename) in all_formats and self.is_allowed_book_file(filename, path, prefix): try: lpath = os.path.join(path, filename).partition(self.normalize_path(prefix))[2] if lpath.startswith(os.sep): lpath = lpath[len(os.sep):] lpath = lpath.replace('\\', '/') idx = bl_cache.get(lpath, None) if idx is not None: bl_cache[lpath] = None if self.update_metadata_item(bl[idx]): # print 'update_metadata_item returned true' changed = True else: if bl.add_book(self.book_from_path(prefix, lpath), replace_metadata=False): changed = True except: # Probably a filename encoding error import traceback traceback.print_exc() return changed if isinstance(ebook_dirs, string_or_bytes): ebook_dirs = [ebook_dirs] for ebook_dir in ebook_dirs: ebook_dir = self.path_to_unicode(ebook_dir) if self.SCAN_FROM_ROOT: ebook_dir = self.normalize_path(prefix) else: ebook_dir = self.normalize_path( os.path.join(prefix, *(ebook_dir.split('/'))) if ebook_dir else prefix) debug_print('USBMS: scan from root', self.SCAN_FROM_ROOT, ebook_dir) if not os.path.exists(ebook_dir): continue # Get all books in the ebook_dir directory if self.SUPPORTS_SUB_DIRS or self.SUPPORTS_SUB_DIRS_FOR_SCAN: # build a list of files to check, so we can accurately report progress flist = [] for path, dirs, files in safe_walk(ebook_dir): for filename in files: if filename != self.METADATA_CACHE: flist.append({'filename': self.path_to_unicode(filename), 'path':self.path_to_unicode(path)}) for i, f in enumerate(flist): self.report_progress(i/float(len(flist)), _('Getting list of books on device...')) changed = update_booklist(f['filename'], f['path'], prefix) if changed: need_sync = True else: paths = os.listdir(ebook_dir) for i, filename in enumerate(paths): self.report_progress((i+1) / float(len(paths)), _('Getting list of books on device...')) changed = update_booklist(self.path_to_unicode(filename), ebook_dir, prefix) if changed: need_sync = True # Remove books that are no longer in the filesystem. Cache contains # indices into the booklist if book not in filesystem, None otherwise # Do the operation in reverse order so indices remain valid for idx in sorted(itervalues(bl_cache), reverse=True, key=lambda x: -1 if x is None else x): if idx is not None: need_sync = True del bl[idx] debug_print('USBMS: count found in cache: %d, count of files in metadata: %d, need_sync: %s' % (len(bl_cache), len(bl), need_sync)) if need_sync: # self.count_found_in_bl != len(bl) or need_sync: if oncard == 'cardb': self.sync_booklists((None, None, bl)) elif oncard == 'carda': self.sync_booklists((None, bl, None)) else: self.sync_booklists((bl, None, None)) self.report_progress(1.0, _('Getting list of books on device...')) debug_print('USBMS: Finished fetching list of books from device. oncard=', oncard) return bl

[documentos] def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): debug_print('USBMS: uploading %d books'%(len(files))) path = self._sanity_check(on_card, files) paths = [] names = iter(names) metadata = iter(metadata) for i, infile in enumerate(files): mdata, fname = next(metadata), next(names) filepath = self.normalize_path(self.create_upload_path(path, mdata, fname)) if not hasattr(infile, 'read'): infile = self.normalize_path(infile) filepath = self.put_file(infile, filepath, replace_file=True) paths.append(filepath) try: self.upload_cover(os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0], mdata, filepath) except: # Failure to upload cover is not catastrophic import traceback traceback.print_exc() self.report_progress((i+1) / float(len(files)), _('Transferring books to device...')) self.report_progress(1.0, _('Transferring books to device...')) debug_print('USBMS: finished uploading %d books'%(len(files))) return list(zip(paths, cycle([on_card])))

[documentos] def upload_cover(self, path, filename, metadata, filepath): ''' Upload book cover to the device. Default implementation does nothing. :param path: The full path to the folder where the associated book is located. :param filename: The name of the book file without the extension. :param metadata: metadata belonging to the book. Use metadata.thumbnail for cover :param filepath: The full path to the e-book file ''' pass

[documentos] def add_books_to_metadata(self, locations, metadata, booklists): debug_print('USBMS: adding metadata for %d books'%(len(metadata))) metadata = iter(metadata) locations = tuple(locations) for i, location in enumerate(locations): self.report_progress((i+1) / float(len(locations)), _('Adding books to device metadata listing...')) info = next(metadata) blist = 2 if location[1] == 'cardb' else 1 if location[1] == 'carda' else 0 # Extract the correct prefix from the pathname. To do this correctly, # we must ensure that both the prefix and the path are normalized # so that the comparison will work. Book's __init__ will fix up # lpath, so we don't need to worry about that here. path = self.normalize_path(location[0]) if self._main_prefix: prefix = self._main_prefix if \ path.startswith(self.normalize_path(self._main_prefix)) else None if not prefix and self._card_a_prefix: prefix = self._card_a_prefix if \ path.startswith(self.normalize_path(self._card_a_prefix)) else None if not prefix and self._card_b_prefix: prefix = self._card_b_prefix if \ path.startswith(self.normalize_path(self._card_b_prefix)) else None if prefix is None: prints('in add_books_to_metadata. Prefix is None!', path, self._main_prefix) continue lpath = path.partition(prefix)[2] if lpath.startswith('/') or lpath.startswith('\\'): lpath = lpath[1:] book = self.book_class(prefix, lpath, other=info) if book.size is None: book.size = os.stat(self.normalize_path(path)).st_size b = booklists[blist].add_book(book, replace_metadata=True) if b: b._new_book = True self.report_progress(1.0, _('Adding books to device metadata listing...')) debug_print('USBMS: finished adding metadata')

def delete_single_book(self, path): os.unlink(path) def delete_extra_book_files(self, path): filepath = os.path.splitext(path)[0] for ext in self.DELETE_EXTS: for x in (filepath, path): x += ext if os.path.exists(x): if os.path.isdir(x): shutil.rmtree(x, ignore_errors=True) else: os.unlink(x) if self.SUPPORTS_SUB_DIRS: try: os.removedirs(os.path.dirname(path)) except: pass

[documentos] def delete_books(self, paths, end_session=True): debug_print('USBMS: deleting %d books'%(len(paths))) for i, path in enumerate(paths): self.report_progress((i+1) / float(len(paths)), _('Removing books from device...')) path = self.normalize_path(path) if os.path.exists(path): # Delete the ebook self.delete_single_book(path) self.delete_extra_book_files(path) self.report_progress(1.0, _('Removing books from device...')) debug_print('USBMS: finished deleting %d books'%(len(paths)))

[documentos] def remove_books_from_metadata(self, paths, booklists): debug_print('USBMS: removing metadata for %d books'%(len(paths))) for i, path in enumerate(paths): self.report_progress((i+1) / float(len(paths)), _('Removing books from device metadata listing...')) for bl in booklists: for book in bl: if path.endswith(book.path): bl.remove_book(book) self.report_progress(1.0, _('Removing books from device metadata listing...')) debug_print('USBMS: finished removing metadata for %d books'%(len(paths)))

# If you override this method and you use book._new_book, then you must # complete the processing before you call this method. The flag is cleared # at the end just before the return

[documentos] def sync_booklists(self, booklists, end_session=True): debug_print('USBMS: starting sync_booklists') json_codec = JsonCodec() if not os.path.exists(self.normalize_path(self._main_prefix)): os.makedirs(self.normalize_path(self._main_prefix)) def write_prefix(prefix, listid): if (prefix is not None and len(booklists) > listid and isinstance(booklists[listid], self.booklist_class)): if not os.path.exists(prefix): os.makedirs(self.normalize_path(prefix)) with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f: json_codec.encode_to_file(f, booklists[listid]) fsync(f) write_prefix(self._main_prefix, 0) write_prefix(self._card_a_prefix, 1) write_prefix(self._card_b_prefix, 2) # Clear the _new_book indication, as we are supposed to be done with # adding books at this point for blist in booklists: if blist is not None: for book in blist: book._new_book = False self.report_progress(1.0, _('Sending metadata to device...')) debug_print('USBMS: finished sync_booklists')

@classmethod def build_template_regexp(cls): from calibre.devices.utils import build_template_regexp return build_template_regexp(cls.save_template()) @classmethod def path_to_unicode(cls, path): if isbytestring(path): path = path.decode(filesystem_encoding) return path

[documentos] @classmethod def normalize_path(cls, path): 'Return path with platform native path separators' if path is None: return None if os.sep == '\\': path = path.replace('/', '\\') else: path = path.replace('\\', '/') return cls.path_to_unicode(path)

@classmethod def parse_metadata_cache(cls, bl, prefix, name): json_codec = JsonCodec() need_sync = False cache_file = cls.normalize_path(os.path.join(prefix, name)) if os.access(cache_file, os.R_OK): try: with open(cache_file, 'rb') as f: json_codec.decode_from_file(f, bl, cls.book_class, prefix) except: import traceback traceback.print_exc() bl = [] need_sync = True else: need_sync = True return need_sync @classmethod def update_metadata_item(cls, book): changed = False size = os.stat(cls.normalize_path(book.path)).st_size if size != book.size: changed = True mi = cls.metadata_from_path(book.path) book.smart_update(mi) book.size = size return changed @classmethod def metadata_from_path(cls, path): return cls.metadata_from_formats([path]) @classmethod def metadata_from_formats(cls, fmts): from calibre.customize.ui import quick_metadata from calibre.ebooks.metadata.meta import metadata_from_formats with quick_metadata: return metadata_from_formats(fmts, force_read_metadata=True, pattern=cls.build_template_regexp()) @classmethod def book_from_path(cls, prefix, lpath): from calibre.ebooks.metadata.book.base import Metadata if cls.settings().read_metadata or cls.MUST_READ_METADATA: mi = cls.metadata_from_path(cls.normalize_path(os.path.join(prefix, lpath))) else: from calibre.ebooks.metadata.meta import metadata_from_filename mi = metadata_from_filename(cls.normalize_path(os.path.basename(lpath)), cls.build_template_regexp()) if mi is None: mi = Metadata(os.path.splitext(os.path.basename(lpath))[0], [_('Unknown')]) size = os.stat(cls.normalize_path(os.path.join(prefix, lpath))).st_size book = cls.book_class(prefix, lpath, other=mi, size=size) return book

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.metadata.book.base

Código fuente para calibre.ebooks.metadata.book.base

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import copy
import traceback

from calibre import prints
from calibre.constants import DEBUG
from calibre.ebooks.metadata.book import ALL_METADATA_FIELDS, SC_COPYABLE_FIELDS, SC_FIELDS_COPY_NOT_NULL, STANDARD_METADATA_FIELDS, TOP_LEVEL_IDENTIFIERS
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.icu import lower as icu_lower
from calibre.utils.icu import sort_key
from calibre.utils.localization import ngettext
from polyglot.builtins import iteritems, string_or_bytes

# Special sets used to optimize the performance of getting and setting
# attributes on Metadata objects
SIMPLE_GET = frozenset(STANDARD_METADATA_FIELDS - TOP_LEVEL_IDENTIFIERS)
SIMPLE_SET = frozenset(SIMPLE_GET - {'identifiers'})

def human_readable(size, precision=2):
    """ Convert a size in bytes into megabytes """
    ans = size/(1024*1024)
    if ans < 0.1:
        return '<0.1 MB'
    return ('%.'+str(precision)+'f'+ ' MB') % ans

NULL_VALUES = {
                'user_metadata': {},
                'cover_data'   : (None, None),
                'tags'         : [],
                'identifiers'  : {},
                'languages'    : [],
                'device_collections': [],
                'author_sort_map': {},
                'authors'      : [_('Unknown')],
                'author_sort'  : _('Unknown'),
                'title'        : _('Unknown'),
                'user_categories' : {},
                'link_maps'    : {},
                'language'     : 'und'
}

field_metadata = FieldMetadata()

def reset_field_metadata():
    global field_metadata
    field_metadata = FieldMetadata()

def ck(typ):
    return icu_lower(typ).strip().replace(':', '').replace(',', '')

def cv(val):
    return val.strip().replace(',', '|')

[documentos] class Metadata: ''' A class representing all the metadata for a book. The various standard metadata fields are available as attributes of this object. You can also stick arbitrary attributes onto this object. Metadata from custom columns should be accessed via the get() method, passing in the lookup name for the column, for example: "#mytags". Use the :meth:`is_null` method to test if a field is null. This object also has functions to format fields into strings. The list of standard metadata fields grows with time is in :data:`STANDARD_METADATA_FIELDS`. Please keep the method based API of this class to a minimum. Every method becomes a reserved field name. ''' __calibre_serializable__ = True def __init__(self, title, authors=(_('Unknown'),), other=None, template_cache=None, formatter=None): ''' @param title: title or ``_('Unknown')`` @param authors: List of strings or [] @param other: None or a metadata object ''' _data = copy.deepcopy(NULL_VALUES) _data.pop('language') object.__setattr__(self, '_data', _data) if other is not None: self.smart_update(other) else: if title: self.title = title if authors: # List of strings or [] self.author = list(authors) if authors else [] # Needed for backward compatibility self.authors = list(authors) if authors else [] from calibre.ebooks.metadata.book.formatter import SafeFormat self.formatter = SafeFormat() if formatter is None else formatter self.template_cache = template_cache

[documentos] def is_null(self, field): ''' Return True if the value of field is null in this object. 'null' means it is unknown or evaluates to False. So a title of _('Unknown') is null or a language of 'und' is null. Be careful with numeric fields since this will return True for zero as well as None. Also returns True if the field does not exist. ''' try: null_val = NULL_VALUES.get(field, None) val = getattr(self, field, None) return not val or val == null_val except: return True

def set_null(self, field): null_val = copy.copy(NULL_VALUES.get(field)) setattr(self, field, null_val) def __getattribute__(self, field): _data = object.__getattribute__(self, '_data') if field in SIMPLE_GET: return _data.get(field, None) if field in TOP_LEVEL_IDENTIFIERS: return _data.get('identifiers').get(field, None) if field == 'language': try: return _data.get('languages', [])[0] except: return NULL_VALUES['language'] try: return object.__getattribute__(self, field) except AttributeError: pass if field in _data['user_metadata']: d = _data['user_metadata'][field] val = d['#value#'] if val is None and d['datatype'] == 'composite': d['#value#'] = 'RECURSIVE_COMPOSITE FIELD (Metadata) ' + field val = d['#value#'] = self.formatter.safe_format( d['display']['composite_template'], self, _('TEMPLATE ERROR'), self, column_name=field, template_cache=self.template_cache).strip() return val if field.startswith('#') and field.endswith('_index'): try: return self.get_extra(field[:-6]) except: pass raise AttributeError( 'Metadata object has no attribute named: '+ repr(field)) def __setattr__(self, field, val, extra=None): _data = object.__getattribute__(self, '_data') if field in SIMPLE_SET: if val is None: val = copy.copy(NULL_VALUES.get(field, None)) _data[field] = val elif field in TOP_LEVEL_IDENTIFIERS: field, val = self._clean_identifier(field, val) identifiers = _data['identifiers'] identifiers.pop(field, None) if val: identifiers[field] = val elif field == 'identifiers': if not val: val = copy.copy(NULL_VALUES.get('identifiers', None)) self.set_identifiers(val) elif field == 'language': langs = [] if val and val.lower() != 'und': langs = [val] _data['languages'] = langs elif field in _data['user_metadata']: d = _data['user_metadata'][field] d['#value#'] = val d['#extra#'] = extra else: # You are allowed to stick arbitrary attributes onto this object as # long as they don't conflict with global or user metadata names # Don't abuse this privilege self.__dict__[field] = val def __iter__(self): return iter(object.__getattribute__(self, '_data')) def has_key(self, key): return key in object.__getattribute__(self, '_data') def _evaluate_all_composites(self): custom_fields = object.__getattribute__(self, '_data')['user_metadata'] for field in custom_fields: self._evaluate_composite(field) def _evaluate_composite(self, field): f = object.__getattribute__(self, '_data')['user_metadata'].get(field, None) if f is not None: if f['datatype'] == 'composite' and f['#value#'] is None: self.get(field)

[documentos] def deepcopy(self, class_generator=lambda : Metadata(None)): ''' Do not use this method unless you know what you are doing, if you want to create a simple clone of this object, use :meth:`deepcopy_metadata` instead. Class_generator must be a function that returns an instance of Metadata or a subclass of it.''' # We don't need to evaluate all the composites here because we # are returning a "real" Metadata instance that has __get_attribute__. m = class_generator() if not isinstance(m, Metadata): return None object.__setattr__(m, '__dict__', copy.deepcopy(self.__dict__)) return m

def deepcopy_metadata(self): # We don't need to evaluate all the composites here because we # are returning a "real" Metadata instance that has __get_attribute__. m = Metadata(None) object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data'))) return m def get(self, field, default=None): try: return self.__getattribute__(field) except AttributeError: return default def get_extra(self, field, default=None): # Don't need to evaluate all composites because a composite can't have # an extra value _data = object.__getattribute__(self, '_data') if field in _data['user_metadata']: try: return _data['user_metadata'][field]['#extra#'] except: return default raise AttributeError( 'Metadata object has no attribute named: '+ repr(field)) def set(self, field, val, extra=None): self.__setattr__(field, val, extra)

[documentos] def get_identifiers(self): ''' Return a copy of the identifiers dictionary. The dict is small, and the penalty for using a reference where a copy is needed is large. Also, we don't want any manipulations of the returned dict to show up in the book. ''' ans = object.__getattribute__(self, '_data')['identifiers'] if not ans: ans = {} return copy.deepcopy(ans)

def _clean_identifier(self, typ, val): if typ: typ = ck(typ) if val: val = cv(val) return typ, val

[documentos] def set_identifiers(self, identifiers): ''' Set all identifiers. Note that if you previously set ISBN, calling this method will delete it. ''' cleaned = {ck(k):cv(v) for k, v in iteritems(identifiers) if k and v} object.__getattribute__(self, '_data')['identifiers'] = cleaned

[documentos] def set_identifier(self, typ, val): 'If val is empty, deletes identifier of type typ' typ, val = self._clean_identifier(typ, val) if not typ: return identifiers = object.__getattribute__(self, '_data')['identifiers'] identifiers.pop(typ, None) if val: identifiers[typ] = val

def has_identifier(self, typ): identifiers = object.__getattribute__(self, '_data')['identifiers'] return typ in identifiers # field-oriented interface. Intended to be the same as in LibraryDatabase

[documentos] def standard_field_keys(self): ''' return a list of all possible keys, even if this book doesn't have them ''' return STANDARD_METADATA_FIELDS

[documentos] def custom_field_keys(self): ''' return a list of the custom fields in this book ''' return iter(object.__getattribute__(self, '_data')['user_metadata'])

[documentos] def all_field_keys(self): ''' All field keys known by this instance, even if their value is None ''' _data = object.__getattribute__(self, '_data') return frozenset(ALL_METADATA_FIELDS.union(frozenset(_data['user_metadata'])))

[documentos] def metadata_for_field(self, key): ''' return metadata describing a standard or custom field. ''' if key not in self.custom_field_keys(): return self.get_standard_metadata(key, make_copy=False) return self.get_user_metadata(key, make_copy=False)

[documentos] def all_non_none_fields(self): ''' Return a dictionary containing all non-None metadata fields, including the custom ones. ''' result = {} _data = object.__getattribute__(self, '_data') for attr in STANDARD_METADATA_FIELDS: v = _data.get(attr, None) if v is not None: result[attr] = v # separate these because it uses the self.get(), not _data.get() for attr in TOP_LEVEL_IDENTIFIERS: v = self.get(attr, None) if v is not None: result[attr] = v for attr in _data['user_metadata']: v = self.get(attr, None) if v is not None: result[attr] = v if _data['user_metadata'][attr]['datatype'] == 'series': result[attr+'_index'] = _data['user_metadata'][attr]['#extra#'] return result

# End of field-oriented interface # Extended interfaces. These permit one to get copies of metadata dictionaries, and to # get and set custom field metadata

[documentos] def get_standard_metadata(self, field, make_copy): ''' return field metadata from the field if it is there. Otherwise return None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict. ''' if field in field_metadata and field_metadata[field]['kind'] == 'field': if make_copy: return copy.deepcopy(field_metadata[field]) return field_metadata[field] return None

[documentos] def get_all_standard_metadata(self, make_copy): ''' return a dict containing all the standard field metadata associated with the book. ''' if not make_copy: return field_metadata res = {} for k in field_metadata: if field_metadata[k]['kind'] == 'field': res[k] = copy.deepcopy(field_metadata[k]) return res

[documentos] def get_all_user_metadata(self, make_copy): ''' return a dict containing all the custom field metadata associated with the book. ''' # Must evaluate all composites because we are returning a dict, not a # Metadata instance self._evaluate_all_composites() _data = object.__getattribute__(self, '_data') user_metadata = _data['user_metadata'] if not make_copy: return user_metadata res = {} for k in user_metadata: res[k] = copy.deepcopy(user_metadata[k]) return res

[documentos] def get_user_metadata(self, field, make_copy): ''' return field metadata from the object if it is there. Otherwise return None. field is the key name, not the label. Return a copy if requested, just in case the user wants to change values in the dict. ''' _data = object.__getattribute__(self, '_data')['user_metadata'] if field in _data: # Must evaluate the field because it might be a composite. It won't # be evaluated on demand because we are returning its dict, not a # Metadata instance self._evaluate_composite(field) if make_copy: return copy.deepcopy(_data[field]) return _data[field] return None

[documentos] def set_all_user_metadata(self, metadata): ''' store custom field metadata into the object. Field is the key name not the label ''' if metadata is None: traceback.print_stack() return um = {} for key, meta in iteritems(metadata): m = meta.copy() if '#value#' not in m: if m['datatype'] == 'text' and m['is_multiple']: m['#value#'] = [] else: m['#value#'] = None um[key] = m _data = object.__getattribute__(self, '_data') _data['user_metadata'] = um

[documentos] def set_user_metadata(self, field, metadata): ''' store custom field metadata for one column into the object. Field is the key name not the label ''' if field is not None: if not field.startswith('#'): raise AttributeError( 'Custom field name %s must begin with \'#\''%repr(field)) if metadata is None: traceback.print_stack() return m = dict(metadata) # Copying the elements should not be necessary. The objects referenced # in the dict should not change. Of course, they can be replaced. # for k,v in iteritems(metadata): # m[k] = copy.copy(v) if '#value#' not in m: if m['datatype'] == 'text' and m['is_multiple']: m['#value#'] = [] else: m['#value#'] = None _data = object.__getattribute__(self, '_data') _data['user_metadata'][field] = m

[documentos] def remove_stale_user_metadata(self, other_mi): ''' Remove user metadata keys (custom column keys) if they don't exist in 'other_mi', which must be a metadata object ''' me = self.get_all_user_metadata(make_copy=False) other = set(other_mi.custom_field_keys()) new = {} for k,v in me.items(): if k in other: new[k] = v self.set_all_user_metadata(new)

[documentos] def template_to_attribute(self, other, ops): ''' Takes a list [(src,dest), (src,dest)], evaluates the template in the context of other, then copies the result to self[dest]. This is on a best-efforts basis. Some assignments can make no sense. ''' if not ops: return from calibre.ebooks.metadata.book.formatter import SafeFormat formatter = SafeFormat() for op in ops: try: src = op[0] dest = op[1] val = formatter.safe_format(src, other, 'PLUGBOARD TEMPLATE ERROR', other) if dest == 'tags': self.set(dest, [f.strip() for f in val.split(',') if f.strip()]) elif dest == 'authors': self.set(dest, [f.strip() for f in val.split('&') if f.strip()]) else: self.set(dest, val) except: if DEBUG: traceback.print_exc()

# Old Metadata API {{{ def print_all_attributes(self): for x in STANDARD_METADATA_FIELDS: prints('%s:'%x, getattr(self, x, 'None')) for x in self.custom_field_keys(): meta = self.get_user_metadata(x, make_copy=False) if meta is not None: prints(x, meta) prints('--------------')

[documentos] def smart_update(self, other, replace_metadata=False): ''' Merge the information in `other` into self. In case of conflicts, the information in `other` takes precedence, unless the information in `other` is NULL. ''' def copy_not_none(dest, src, attr): v = getattr(src, attr, None) if v not in (None, NULL_VALUES.get(attr, None)): setattr(dest, attr, copy.deepcopy(v)) unknown = _('Unknown') if other.title and other.title != unknown: self.title = other.title if hasattr(other, 'title_sort'): self.title_sort = other.title_sort if other.authors and ( other.authors[0] != unknown or ( not self.authors or ( len(self.authors) == 1 and self.authors[0] == unknown and getattr(self, 'author_sort', None) == unknown ) ) ): self.authors = list(other.authors) if hasattr(other, 'author_sort_map'): self.author_sort_map = dict(other.author_sort_map) if hasattr(other, 'author_sort'): self.author_sort = other.author_sort if replace_metadata: # SPECIAL_FIELDS = frozenset(['lpath', 'size', 'comments', 'thumbnail']) for attr in SC_COPYABLE_FIELDS: setattr(self, attr, getattr(other, attr, 1.0 if attr == 'series_index' else None)) self.tags = other.tags self.cover_data = getattr(other, 'cover_data', NULL_VALUES['cover_data']) self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True)) for x in SC_FIELDS_COPY_NOT_NULL: copy_not_none(self, other, x) if callable(getattr(other, 'get_identifiers', None)): self.set_identifiers(other.get_identifiers()) # language is handled below else: for attr in SC_COPYABLE_FIELDS: copy_not_none(self, other, attr) for x in SC_FIELDS_COPY_NOT_NULL: copy_not_none(self, other, x) if other.tags: # Case-insensitive but case preserving merging lotags = [t.lower() for t in other.tags] lstags = [t.lower() for t in self.tags] ot, st = map(frozenset, (lotags, lstags)) for t in st.intersection(ot): sidx = lstags.index(t) oidx = lotags.index(t) self.tags[sidx] = other.tags[oidx] self.tags += [t for t in other.tags if t.lower() in ot-st] if getattr(other, 'cover_data', False): other_cover = other.cover_data[-1] self_cover = self.cover_data[-1] if self.cover_data else b'' if not self_cover: self_cover = b'' if not other_cover: other_cover = b'' if len(other_cover) > len(self_cover): self.cover_data = other.cover_data if callable(getattr(other, 'custom_field_keys', None)): for x in other.custom_field_keys(): meta = other.get_user_metadata(x, make_copy=True) if meta is not None: self_tags = self.get(x, []) if isinstance(self_tags, string_or_bytes): self_tags = [] self.set_user_metadata(x, meta) # get... did the deepcopy other_tags = other.get(x, []) if meta['datatype'] == 'text' and meta['is_multiple']: # Case-insensitive but case preserving merging lotags = [t.lower() for t in other_tags] try: lstags = [t.lower() for t in self_tags] except TypeError: # Happens if x is not a text, is_multiple field # on self lstags = [] self_tags = [] ot, st = map(frozenset, (lotags, lstags)) for t in st.intersection(ot): sidx = lstags.index(t) oidx = lotags.index(t) self_tags[sidx] = other_tags[oidx] self_tags += [t for t in other_tags if t.lower() in ot-st] setattr(self, x, self_tags) my_comments = getattr(self, 'comments', '') other_comments = getattr(other, 'comments', '') if not my_comments: my_comments = '' if not other_comments: other_comments = '' if len(other_comments.strip()) > len(my_comments.strip()): self.comments = other_comments # Copy all the non-none identifiers if callable(getattr(other, 'get_identifiers', None)): d = self.get_identifiers() s = other.get_identifiers() d.update([v for v in iteritems(s) if v[1] is not None]) self.set_identifiers(d) else: # other structure not Metadata. Copy the top-level identifiers for attr in TOP_LEVEL_IDENTIFIERS: copy_not_none(self, other, attr) other_lang = getattr(other, 'languages', []) if other_lang and other_lang != ['und']: self.languages = list(other_lang) if not getattr(self, 'series', None): self.series_index = None

def format_series_index(self, val=None): from calibre.ebooks.metadata import fmt_sidx v = self.series_index if val is None else val try: x = float(v) except Exception: x = 1 return fmt_sidx(x) def authors_from_string(self, raw): from calibre.ebooks.metadata import string_to_authors self.authors = string_to_authors(raw) def format_authors(self): from calibre.ebooks.metadata import authors_to_string return authors_to_string(self.authors) def format_tags(self): return ', '.join([str(t) for t in sorted(self.tags, key=sort_key)]) def format_rating(self, v=None, divide_by=1): if v is None: if self.rating is not None: return str(self.rating/divide_by) return 'None' return str(v/divide_by)

[documentos] def format_field(self, key, series_with_index=True): ''' Returns the tuple (display_name, formatted_value) ''' name, val, ign, ign = self.format_field_extended(key, series_with_index) return (name, val)

def format_field_extended(self, key, series_with_index=True): from calibre.ebooks.metadata import authors_to_string ''' returns the tuple (display_name, formatted_value, original_value, field_metadata) ''' from calibre.utils.date import format_date # Handle custom series index if key.startswith('#') and key.endswith('_index'): tkey = key[:-6] # strip the _index cmeta = self.get_user_metadata(tkey, make_copy=False) if cmeta and cmeta['datatype'] == 'series': if self.get(tkey): res = self.get_extra(tkey) return (str(cmeta['name']+'_index'), self.format_series_index(res), res, cmeta) else: return (str(cmeta['name']+'_index'), '', '', cmeta) if key in self.custom_field_keys(): res = self.get(key, None) # get evaluates all necessary composites cmeta = self.get_user_metadata(key, make_copy=False) name = str(cmeta['name']) if res is None or res == '': # can't check "not res" because of numeric fields return (name, res, None, None) orig_res = res datatype = cmeta['datatype'] if datatype == 'text' and cmeta['is_multiple']: res = cmeta['is_multiple']['list_to_ui'].join(res) elif datatype == 'series' and series_with_index: if self.get_extra(key) is not None: res = res + \ ' [%s]'%self.format_series_index(val=self.get_extra(key)) elif datatype == 'datetime': res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'bool': res = _('Yes') if res else _('No') elif datatype == 'rating': res = '%.2g'%(res/2) elif datatype in ['int', 'float']: try: fmt = cmeta['display'].get('number_format', None) res = fmt.format(res) except: pass return (name, str(res), orig_res, cmeta) # convert top-level ids into their value if key in TOP_LEVEL_IDENTIFIERS: fmeta = field_metadata['identifiers'] name = key res = self.get(key, None) return (name, res, res, fmeta) # Translate aliases into the standard field name fmkey = field_metadata.search_term_to_field_key(key) if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field': res = self.get(key, None) fmeta = field_metadata[fmkey] name = str(fmeta['name']) if res is None or res == '': return (name, res, None, None) orig_res = res name = str(fmeta['name']) datatype = fmeta['datatype'] if key == 'authors': res = authors_to_string(res) elif key == 'series_index': res = self.format_series_index(res) elif datatype == 'text' and fmeta['is_multiple']: if isinstance(res, dict): res = [k + ':' + v for k,v in res.items()] res = fmeta['is_multiple']['list_to_ui'].join(sorted(filter(None, res), key=sort_key)) elif datatype == 'series' and series_with_index: res = res + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'rating': res = '%.2g'%(res/2) elif key == 'size': res = human_readable(res) return (name, str(res), orig_res, fmeta) return (None, None, None, None) def __unicode__representation__(self): ''' A string representation of this object, suitable for printing to console ''' from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = [] def fmt(x, y): ans.append('%-20s: %s'%(str(x), str(y))) fmt('Title', self.title) if self.title_sort: fmt('Title sort', self.title_sort) if self.authors: fmt('Author(s)', authors_to_string(self.authors) + ((' [' + self.author_sort + ']') if self.author_sort and self.author_sort != _('Unknown') else '')) if self.publisher: fmt('Publisher', self.publisher) if getattr(self, 'book_producer', False): fmt('Book Producer', self.book_producer) if self.tags: fmt('Tags', ', '.join([str(t) for t in self.tags])) if self.series: fmt('Series', self.series + ' #%s'%self.format_series_index()) if not self.is_null('languages'): fmt('Languages', ', '.join(self.languages)) if self.rating is not None: fmt('Rating', ('%.2g'%(float(self.rating)/2)) if self.rating else '') if self.timestamp is not None: fmt('Timestamp', isoformat(self.timestamp)) if self.pubdate is not None: fmt('Published', isoformat(self.pubdate)) if self.rights is not None: fmt('Rights', str(self.rights)) if self.identifiers: fmt('Identifiers', ', '.join(['%s:%s'%(k, v) for k, v in iteritems(self.identifiers)])) if self.comments: fmt('Comments', self.comments) for key in self.custom_field_keys(): val = self.get(key, None) if val: (name, val) = self.format_field(key) fmt(name, str(val)) return '\n'.join(ans)

[documentos] def to_html(self): ''' A HTML representation of this object. ''' from calibre.ebooks.metadata import authors_to_string from calibre.utils.date import isoformat ans = [(_('Title'), str(self.title))] ans += [(_('Author(s)'), (authors_to_string(self.authors) if self.authors else _('Unknown')))] ans += [(_('Publisher'), str(self.publisher))] ans += [(_('Producer'), str(self.book_producer))] ans += [(_('Comments'), str(self.comments))] ans += [('ISBN', str(self.isbn))] ans += [(_('Tags'), ', '.join([str(t) for t in self.tags]))] if self.series: ans += [(ngettext('Series', 'Series', 1), str(self.series) + ' #%s'%self.format_series_index())] ans += [(_('Languages'), ', '.join(self.languages))] if self.timestamp is not None: ans += [(_('Timestamp'), str(isoformat(self.timestamp, as_utc=False, sep=' ')))] if self.pubdate is not None: ans += [(_('Published'), str(isoformat(self.pubdate, as_utc=False, sep=' ')))] if self.rights is not None: ans += [(_('Rights'), str(self.rights))] for key in self.custom_field_keys(): val = self.get(key, None) if val: (name, val) = self.format_field(key) ans += [(name, val)] for i, x in enumerate(ans): ans[i] = '<tr><td><b>%s</b></td><td>%s</td></tr>'%x return '<table>%s</table>'%'\n'.join(ans)

__str__ = __unicode__representation__ def __nonzero__(self): return bool(self.title or self.author or self.comments or self.tags) __bool__ = __nonzero__

    # }}}

def field_from_string(field, raw, field_metadata):
    ''' Parse the string raw to return an object that is suitable for calling
    set() on a Metadata object. '''
    dt = field_metadata['datatype']
    val = object
    if dt in {'int', 'float'}:
        val = int(raw) if dt == 'int' else float(raw)
    elif dt == 'rating':
        val = float(raw) * 2
    elif dt == 'datetime':
        from calibre.utils.iso8601 import parse_iso8601
        try:
            val = parse_iso8601(raw, require_aware=True)
        except Exception:
            from calibre.utils.date import parse_only_date
            val = parse_only_date(raw)
    elif dt == 'bool':
        if raw.lower() in {'true', 'yes', 'y'}:
            val = True
        elif raw.lower() in {'false', 'no', 'n'}:
            val = False
        else:
            raise ValueError('Unknown value for %s: %s'%(field, raw))
    elif dt == 'text':
        ism = field_metadata['is_multiple']
        if ism:
            val = [x.strip() for x in raw.split(ism['ui_to_list'])]
            if field == 'identifiers':
                val = {x.partition(':')[0]:x.partition(':')[-1] for x in val}
            elif field == 'languages':
                from calibre.utils.localization import canonicalize_lang
                val = [canonicalize_lang(x) for x in val]
                val = [x for x in val if x]
    if val is object:
        val = raw
    return val

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.metadata.sources.base

Código fuente para calibre.ebooks.metadata.sources.base

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import absolute_import, division, print_function, unicode_literals

__license__   = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re
import threading
from functools import total_ordering

from calibre import browser, random_user_agent
from calibre.customize import Plugin
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.author_mapper import cap_author_token
from calibre.utils.localization import canonicalize_lang, get_lang
from polyglot.builtins import cmp, iteritems

def create_log(ostream=None):
    from calibre.utils.logging import FileStream, ThreadSafeLog
    log = ThreadSafeLog(level=ThreadSafeLog.DEBUG)
    log.outputs = [FileStream(ostream)]
    return log

# Comparing Metadata objects for relevance {{{
words = ("the", "a", "an", "of", "and")
prefix_pat = re.compile(r'^(%s)\s+'%("|".join(words)))
trailing_paren_pat = re.compile(r'\(.*\)$')
whitespace_pat = re.compile(r'\s+')

def cleanup_title(s):
    if not s:
        s = _('Unknown')
    s = s.strip().lower()
    s = prefix_pat.sub(' ', s)
    s = trailing_paren_pat.sub('', s)
    s = whitespace_pat.sub(' ', s)
    return s.strip()

[documentos] @total_ordering class InternalMetadataCompareKeyGen: ''' Generate a sort key for comparison of the relevance of Metadata objects, given a search query. This is used only to compare results from the same metadata source, not across different sources. The sort key ensures that an ascending order sort is a sort by order of decreasing relevance. The algorithm is: * Prefer results that have at least one identifier the same as for the query * Prefer results with a cached cover URL * Prefer results with all available fields filled in * Prefer results with the same language as the current user interface language * Prefer results that are an exact title match to the query * Prefer results with longer comments (greater than 10% longer) * Use the relevance of the result as reported by the metadata source's search engine ''' def __init__(self, mi, source_plugin, title, authors, identifiers): same_identifier = 2 idents = mi.get_identifiers() for k, v in iteritems(identifiers): if idents.get(k) == v: same_identifier = 1 break all_fields = 1 if source_plugin.test_fields(mi) is None else 2 exact_title = 1 if title and \ cleanup_title(title) == cleanup_title(mi.title) else 2 language = 1 if mi.language: mil = canonicalize_lang(mi.language) if mil != 'und' and mil != canonicalize_lang(get_lang()): language = 2 has_cover = 2 if (not source_plugin.cached_cover_url_is_reliable or source_plugin.get_cached_cover_url(mi.identifiers) is None) else 1 self.base = (same_identifier, has_cover, all_fields, language, exact_title) self.comments_len = len((mi.comments or '').strip()) self.extra = getattr(mi, 'source_relevance', 0) def compare_to_other(self, other): a = cmp(self.base, other.base) if a != 0: return a cx, cy = self.comments_len, other.comments_len if cx and cy: t = (cx + cy) / 20 delta = cy - cx if abs(delta) > t: return -1 if delta < 0 else 1 return cmp(self.extra, other.extra) def __eq__(self, other): return self.compare_to_other(other) == 0 def __ne__(self, other): return self.compare_to_other(other) != 0 def __lt__(self, other): return self.compare_to_other(other) < 0 def __le__(self, other): return self.compare_to_other(other) <= 0 def __gt__(self, other): return self.compare_to_other(other) > 0 def __ge__(self, other): return self.compare_to_other(other) >= 0

# }}}

def get_cached_cover_urls(mi):
    from calibre.customize.ui import metadata_plugins
    plugins = list(metadata_plugins(['identify']))
    for p in plugins:
        url = p.get_cached_cover_url(mi.identifiers)
        if url:
            yield (p, url)

def dump_caches():
    from calibre.customize.ui import metadata_plugins
    return {p.name:p.dump_caches() for p in metadata_plugins(['identify'])}

def load_caches(dump):
    from calibre.customize.ui import metadata_plugins
    plugins = list(metadata_plugins(['identify']))
    for p in plugins:
        cache = dump.get(p.name, None)
        if cache:
            p.load_caches(cache)

def fixauthors(authors):
    if not authors:
        return authors
    ans = []
    for x in authors:
        ans.append(' '.join(map(cap_author_token, x.split())))
    return ans

def fixcase(x):
    if x:
        from calibre.utils.titlecase import titlecase
        x = titlecase(x)
    return x

class Option:
    __slots__ = ['type', 'default', 'label', 'desc', 'name', 'choices']

    def __init__(self, name, type_, default, label, desc, choices=None):
        '''
        :param name: The name of this option. Must be a valid python identifier
        :param type_: The type of this option, one of ('number', 'string',
                        'bool', 'choices')
        :param default: The default value for this option
        :param label: A short (few words) description of this option
        :param desc: A longer description of this option
        :param choices: A dict of possible values, used only if type='choices'.
        dict is of the form {key:human readable label, ...}
        '''
        self.name, self.type, self.default, self.label, self.desc = (name,
                type_, default, label, desc)
        if choices and not isinstance(choices, dict):
            choices = dict([(x, x) for x in choices])
        self.choices = choices

[documentos] class Source(Plugin): type = _('Metadata source') author = 'Kovid Goyal' supported_platforms = ['windows', 'osx', 'linux'] #: Set of capabilities supported by this plugin. #: Useful capabilities are: 'identify', 'cover' capabilities = frozenset() #: List of metadata fields that can potentially be download by this plugin #: during the identify phase touched_fields = frozenset() #: Set this to True if your plugin returns HTML formatted comments has_html_comments = False #: Setting this to True means that the browser object will indicate #: that it supports gzip transfer encoding. This can speedup downloads #: but make sure that the source actually supports gzip transfer encoding #: correctly first supports_gzip_transfer_encoding = False #: Set this to True to ignore HTTPS certificate errors when connecting #: to this source. ignore_ssl_errors = False #: Cached cover URLs can sometimes be unreliable (i.e. the download could #: fail or the returned image could be bogus). If that is often the case #: with this source, set to False cached_cover_url_is_reliable = True #: A list of :class:`Option` objects. They will be used to automatically #: construct the configuration widget for this plugin options = () #: A string that is displayed at the top of the config widget for this #: plugin config_help_message = None #: If True this source can return multiple covers for a given query can_get_multiple_covers = False #: If set to True covers downloaded by this plugin are automatically trimmed. auto_trim_covers = False #: If set to True, and this source returns multiple results for a query, #: some of which have ISBNs and some of which do not, the results without #: ISBNs will be ignored prefer_results_with_isbn = True def __init__(self, *args, **kwargs): Plugin.__init__(self, *args, **kwargs) self.running_a_test = False # Set to True when using identify_test() self._isbn_to_identifier_cache = {} self._identifier_to_cover_url_cache = {} self.cache_lock = threading.RLock() self._config_obj = None self._browser = None self.prefs.defaults['ignore_fields'] = [] for opt in self.options: self.prefs.defaults[opt.name] = opt.default # Configuration {{{

[documentos] def is_configured(self): ''' Return False if your plugin needs to be configured before it can be used. For example, it might need a username/password/API key. ''' return True

def is_customizable(self): return True

[documentos] def customization_help(self): return 'This plugin can only be customized using the GUI'

[documentos] def config_widget(self): from calibre.gui2.metadata.config import ConfigWidget return ConfigWidget(self)

[documentos] def save_settings(self, config_widget): config_widget.commit()

@property def prefs(self): if self._config_obj is None: from calibre.utils.config import JSONConfig self._config_obj = JSONConfig('metadata_sources/%s.json'%self.name) return self._config_obj # }}} # Browser {{{ @property def user_agent(self): # Pass in an index to random_user_agent() to test with a particular # user agent return random_user_agent() @property def browser(self): if self._browser is None: self._browser = browser(user_agent=self.user_agent, verify_ssl_certificates=not self.ignore_ssl_errors) if self.supports_gzip_transfer_encoding: self._browser.set_handle_gzip(True) return self._browser.clone_browser() # }}} # Caching {{{ def get_related_isbns(self, id_): with self.cache_lock: for isbn, q in iteritems(self._isbn_to_identifier_cache): if q == id_: yield isbn def cache_isbn_to_identifier(self, isbn, identifier): with self.cache_lock: self._isbn_to_identifier_cache[isbn] = identifier def cached_isbn_to_identifier(self, isbn): with self.cache_lock: return self._isbn_to_identifier_cache.get(isbn, None) def cache_identifier_to_cover_url(self, id_, url): with self.cache_lock: self._identifier_to_cover_url_cache[id_] = url def cached_identifier_to_cover_url(self, id_): with self.cache_lock: return self._identifier_to_cover_url_cache.get(id_, None) def dump_caches(self): with self.cache_lock: return {'isbn_to_identifier':self._isbn_to_identifier_cache.copy(), 'identifier_to_cover':self._identifier_to_cover_url_cache.copy()} def load_caches(self, dump): with self.cache_lock: self._isbn_to_identifier_cache.update(dump['isbn_to_identifier']) self._identifier_to_cover_url_cache.update(dump['identifier_to_cover']) # }}} # Utility functions {{{

[documentos] def get_author_tokens(self, authors, only_first_author=True): ''' Take a list of authors and return a list of tokens useful for an AND search query. This function tries to return tokens in first name middle names last name order, by assuming that if a comma is in the author name, the name is in lastname, other names form. ''' if authors: # Leave ' in there for Irish names remove_pat = re.compile(r'[!@#$%^&*()（）「」{}`~"\s\[\]/]') replace_pat = re.compile(r'[-+.:;,，。；：]') if only_first_author: authors = authors[:1] for au in authors: has_comma = ',' in au au = replace_pat.sub(' ', au) parts = au.split() if has_comma: # au probably in ln, fn form parts = parts[1:] + parts[:1] for tok in parts: tok = remove_pat.sub('', tok).strip() if len(tok) > 2 and tok.lower() not in ('von', 'van', _('Unknown').lower()): yield tok

[documentos] def get_title_tokens(self, title, strip_joiners=True, strip_subtitle=False): ''' Take a title and return a list of tokens useful for an AND search query. Excludes connectives(optionally) and punctuation. ''' if title: # strip sub-titles if strip_subtitle: subtitle = re.compile(r'([\(\[\{].*?[\)\]\}]|[/:\\].*$)') if len(subtitle.sub('', title)) > 1: title = subtitle.sub('', title) title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in [ # Remove things like: (2010) (Omnibus) etc. (r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|audiobook|audio\scd|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''), # Remove any strings that contain the substring edition inside # parentheses (r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''), # Remove commas used a separators in numbers (r'(\d+),(\d+)', r'\1\2'), # Remove hyphens only if they have whitespace before them (r'(\s-)', ' '), # Replace other special chars with a space (r'''[:,;!@$%^&*(){}.`~"\s\[\]/]《》「」“”''', ' '), ]] for pat, repl in title_patterns: title = pat.sub(repl, title) tokens = title.split() for token in tokens: token = token.strip().strip('"').strip("'") if token and (not strip_joiners or token.lower() not in ('a', 'and', 'the', '&')): yield token

[documentos] def split_jobs(self, jobs, num): 'Split a list of jobs into at most num groups, as evenly as possible' groups = [[] for i in range(num)] jobs = list(jobs) while jobs: for gr in groups: try: job = jobs.pop() except IndexError: break gr.append(job) return [g for g in groups if g]

[documentos] def test_fields(self, mi): ''' Return the first field from self.touched_fields that is null on the mi object ''' for key in self.touched_fields: if key.startswith('identifier:'): key = key.partition(':')[-1] if not mi.has_identifier(key): return 'identifier: ' + key elif mi.is_null(key): return key

[documentos] def clean_downloaded_metadata(self, mi): ''' Call this method in your plugin's identify method to normalize metadata before putting the Metadata object into result_queue. You can of course, use a custom algorithm suited to your metadata source. ''' docase = mi.language == 'eng' or mi.is_null('language') if docase and mi.title: mi.title = fixcase(mi.title) mi.authors = fixauthors(mi.authors) if mi.tags and docase: mi.tags = list(map(fixcase, mi.tags)) mi.isbn = check_isbn(mi.isbn)

def download_multiple_covers(self, title, authors, urls, get_best_cover, timeout, result_queue, abort, log, prefs_name='max_covers'): if not urls: log('No images found for, title: %r and authors: %r'%(title, authors)) return import time from threading import Thread if prefs_name: urls = urls[:self.prefs[prefs_name]] if get_best_cover: urls = urls[:1] log('Downloading %d covers'%len(urls)) workers = [Thread(target=self.download_image, args=(u, timeout, log, result_queue)) for u in urls] for w in workers: w.daemon = True w.start() alive = True start_time = time.time() while alive and not abort.is_set() and time.time() - start_time < timeout: alive = False for w in workers: if w.is_alive(): alive = True break abort.wait(0.1) def download_image(self, url, timeout, log, result_queue): try: ans = self.browser.open_novisit(url, timeout=timeout).read() result_queue.put((self, ans)) log('Downloaded cover from: %s'%url) except Exception: self.log.exception('Failed to download cover from: %r'%url) # }}} # Metadata API {{{

[documentos] def get_book_url(self, identifiers): ''' Return a 3-tuple or None. The 3-tuple is of the form: (identifier_type, identifier_value, URL). The URL is the URL for the book identified by identifiers at this source. identifier_type, identifier_value specify the identifier corresponding to the URL. This URL must be browsable to by a human using a browser. It is meant to provide a clickable link for the user to easily visit the books page at this source. If no URL is found, return None. This method must be quick, and consistent, so only implement it if it is possible to construct the URL from a known scheme given identifiers. ''' return None

[documentos] def get_book_url_name(self, idtype, idval, url): ''' Return a human readable name from the return value of get_book_url(). ''' return self.name

[documentos] def get_book_urls(self, identifiers): ''' Override this method if you would like to return multiple URLs for this book. Return a list of 3-tuples. By default this method simply calls :func:`get_book_url`. ''' data = self.get_book_url(identifiers) if data is None: return () return (data,)

[documentos] def get_cached_cover_url(self, identifiers): ''' Return cached cover URL for the book identified by the identifiers dictionary or None if no such URL exists. Note that this method must only return validated URLs, i.e. not URLS that could result in a generic cover image or a not found error. ''' return None

[documentos] def id_from_url(self, url): ''' Parse a URL and return a tuple of the form: (identifier_type, identifier_value). If the URL does not match the pattern for the metadata source, return None. ''' return None

[documentos] def identify_results_keygen(self, title=None, authors=None, identifiers={}): ''' Return a function that is used to generate a key that can sort Metadata objects by their relevance given a search query (title, authors, identifiers). These keys are used to sort the results of a call to :meth:`identify`. For details on the default algorithm see :class:`InternalMetadataCompareKeyGen`. Re-implement this function in your plugin if the default algorithm is not suitable. ''' def keygen(mi): return InternalMetadataCompareKeyGen(mi, self, title, authors, identifiers) return keygen

[documentos] def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30): ''' Identify a book by its Title/Author/ISBN/etc. If identifiers(s) are specified and no match is found and this metadata source does not store all related identifiers (for example, all ISBNs of a book), this method should retry with just the title and author (assuming they were specified). If this metadata source also provides covers, the URL to the cover should be cached so that a subsequent call to the get covers API with the same ISBN/special identifier does not need to get the cover URL again. Use the caching API for this. Every Metadata object put into result_queue by this method must have a `source_relevance` attribute that is an integer indicating the order in which the results were returned by the metadata source for this query. This integer will be used by :meth:`compare_identify_results`. If the order is unimportant, set it to zero for every result. Make sure that any cover/ISBN mapping information is cached before the Metadata object is put into result_queue. :param log: A log object, use it to output debugging information/errors :param result_queue: A result Queue, results should be put into it. Each result is a Metadata object :param abort: If abort.is_set() returns True, abort further processing and return as soon as possible :param title: The title of the book, can be None :param authors: A list of authors of the book, can be None :param identifiers: A dictionary of other identifiers, most commonly {'isbn':'1234...'} :param timeout: Timeout in seconds, no network request should hang for longer than timeout. :return: None if no errors occurred, otherwise a unicode representation of the error suitable for showing to the user ''' return None

[documentos] def download_cover(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False): ''' Download a cover and put it into result_queue. The parameters all have the same meaning as for :meth:`identify`. Put (self, cover_data) into result_queue. This method should use cached cover URLs for efficiency whenever possible. When cached data is not present, most plugins simply call identify and use its results. If the parameter get_best_cover is True and this plugin can get multiple covers, it should only get the "best" one. ''' pass

    # }}}

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.container

Código fuente para calibre.ebooks.oeb.polish.container

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>

import errno
import hashlib
import logging
import os
import re
import shutil
import sys
import unicodedata
import uuid
from collections import defaultdict
from io import BytesIO
from itertools import count

from css_parser import getUrls, replaceUrls

from calibre import CurrentDir, walk
from calibre.constants import iswindows
from calibre.customize.ui import plugin_for_input_format, plugin_for_output_format
from calibre.ebooks import escape_xpath_attr
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.conversion.plugins.epub_input import ADOBE_OBFUSCATION, IDPF_OBFUSCATION, decrypt_font_data
from calibre.ebooks.conversion.preprocess import CSSPreProcessor as cssp
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.ebooks.metadata.opf3 import CALIBRE_PREFIX, ensure_prefix, items_with_property, read_prefixes
from calibre.ebooks.metadata.utils import parse_opf_version
from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.reader.headers import MetadataHeader
from calibre.ebooks.oeb.base import (
    DC11_NS,
    OEB_DOCS,
    OEB_STYLES,
    OPF,
    OPF2_NS,
    Manifest,
    itercsslinks,
    iterlinks,
    rewrite_links,
    serialize,
    urlquote,
    urlunquote,
)
from calibre.ebooks.oeb.parse_utils import NotHTML, parse_html
from calibre.ebooks.oeb.polish.errors import DRMError, InvalidBook
from calibre.ebooks.oeb.polish.parsing import parse as parse_html_tweak
from calibre.ebooks.oeb.polish.utils import OEB_FONTS, CommentFinder, PositionFinder, adjust_mime_for_epub, guess_type, parse_css
from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile
from calibre.utils.filenames import hardlink_file, nlinks_file, retry_on_fail
from calibre.utils.ipc.simple_worker import WorkerError, fork_job
from calibre.utils.logging import default_log
from calibre.utils.xml_parse import safe_xml_fromstring
from calibre.utils.zipfile import ZipFile
from polyglot.builtins import iteritems
from polyglot.urllib import urlparse

exists, join, relpath = os.path.exists, os.path.join, os.path.relpath
OPF_NAMESPACES = {'opf':OPF2_NS, 'dc':DC11_NS}
null = object()
OEB_FONTS  # for plugin compat

class CSSPreProcessor(cssp):

    def __call__(self, data):
        return self.MS_PAT.sub(self.ms_sub, data)

def clone_dir(src, dest):
    ' Clone a folder using hard links for the files, dest must already exist '
    for x in os.listdir(src):
        dpath = os.path.join(dest, x)
        spath = os.path.join(src, x)
        if os.path.isdir(spath):
            os.mkdir(dpath)
            clone_dir(spath, dpath)
        else:
            try:
                hardlink_file(spath, dpath)
            except:
                shutil.copy2(spath, dpath)

def clone_container(container, dest_dir):
    ' Efficiently clone a container using hard links '
    dest_dir = os.path.abspath(os.path.realpath(dest_dir))
    clone_data = container.clone_data(dest_dir)
    cls = type(container)
    if cls is Container:
        return cls(None, None, container.log, clone_data=clone_data)
    return cls(None, container.log, clone_data=clone_data)

def name_to_abspath(name, root):
    return os.path.abspath(join(root, *name.split('/')))

def abspath_to_name(path, root):
    return relpath(os.path.abspath(path), root).replace(os.sep, '/')

def name_to_href(name, root, base=None, quote=urlquote):
    fullpath = name_to_abspath(name, root)
    basepath = root if base is None else os.path.dirname(name_to_abspath(base, root))
    path = relpath(fullpath, basepath).replace(os.sep, '/')
    return quote(path)

def href_to_name(href, root, base=None):
    base = root if base is None else os.path.dirname(name_to_abspath(base, root))
    try:
        purl = urlparse(href)
    except ValueError:
        return None
    if purl.scheme or not purl.path:
        return None
    href = urlunquote(purl.path)
    if iswindows and ':' in href:
        # path manipulations on windows fail for paths with : in them, so we
        # assume all such paths are invalid/absolute paths.
        return None
    fullpath = os.path.join(base, *href.split('/'))
    try:
        return unicodedata.normalize('NFC', abspath_to_name(fullpath, root))
    except ValueError:
        return None

class ContainerBase:  # {{{
    '''
    A base class that implements just the parsing methods. Useful to create
    virtual containers for testing.
    '''

    #: The mode used to parse HTML and CSS (polishing uses tweak_mode=False and the editor uses tweak_mode=True)
    tweak_mode = False

    def __init__(self, log):
        self.log = log
        self.parsed_cache = {}
        self.mime_map = {}
        self.encoding_map = {}
        self.html_preprocessor = HTMLPreProcessor()
        self.css_preprocessor = CSSPreProcessor()

    def guess_type(self, name):
        ' Return the expected mimetype for the specified file name based on its extension. '
        return adjust_mime_for_epub(filename=name, opf_version=self.opf_version_parsed)

    def decode(self, data, normalize_to_nfc=True):
        """
        Automatically decode ``data`` into a ``unicode`` object.

        :param normalize_to_nfc: Normalize returned unicode to the NFC normal form as is required by both the EPUB and AZW3 formats.
        """
        def fix_data(d):
            return d.replace('\r\n', '\n').replace('\r', '\n')
        if isinstance(data, str):
            return fix_data(data)
        bom_enc = None
        if data[:4] in {b'\0\0\xfe\xff', b'\xff\xfe\0\0'}:
            bom_enc = {b'\0\0\xfe\xff':'utf-32-be',
                       b'\xff\xfe\0\0':'utf-32-le'}[data[:4]]
            data = data[4:]
        elif data[:2] in {b'\xff\xfe', b'\xfe\xff'}:
            bom_enc = {b'\xff\xfe':'utf-16-le', b'\xfe\xff':'utf-16-be'}[data[:2]]
            data = data[2:]
        elif data[:3] == b'\xef\xbb\xbf':
            bom_enc = 'utf-8'
            data = data[3:]
        if bom_enc is not None:
            try:
                self.used_encoding = bom_enc
                return fix_data(data.decode(bom_enc))
            except UnicodeDecodeError:
                pass
        try:
            self.used_encoding = 'utf-8'
            return fix_data(data.decode('utf-8'))
        except UnicodeDecodeError:
            pass
        data, self.used_encoding = xml_to_unicode(data)
        if normalize_to_nfc:
            data = unicodedata.normalize('NFC', data)
        return fix_data(data)

    def parse_xml(self, data):
        data, self.used_encoding = xml_to_unicode(
            data, strip_encoding_pats=True, assume_utf8=True, resolve_entities=True)
        data = unicodedata.normalize('NFC', data)
        return safe_xml_fromstring(data)

    def parse_xhtml(self, data, fname='<string>', force_html5_parse=False):
        if self.tweak_mode:
            return parse_html_tweak(data, log=self.log, decoder=self.decode, force_html5_parse=force_html5_parse)
        else:
            try:
                return parse_html(
                    data, log=self.log, decoder=self.decode,
                    preprocessor=self.html_preprocessor, filename=fname,
                    non_html_file_tags={'ncx'})
            except NotHTML:
                return self.parse_xml(data)

    def parse_css(self, data, fname='<string>', is_declaration=False):
        return parse_css(data, fname=fname, is_declaration=is_declaration, decode=self.decode, log_level=logging.WARNING,
                         css_preprocessor=(None if self.tweak_mode else self.css_preprocessor))
# }}}

[documentos] class Container(ContainerBase): # {{{ ''' A container represents an open e-book as a folder full of files and an OPF file. There are two important concepts: * The root folder. This is the base of the e-book. All the e-books files are inside this folder or in its sub-folders. * Names: These are paths to the books' files relative to the root folder. They always contain POSIX separators and are unquoted. They can be thought of as canonical identifiers for files in the book. Most methods on the container object work with names. Names are always in the NFC Unicode normal form. * Clones: the container object supports efficient on-disk cloning, which is used to implement checkpoints in the e-book editor. In order to make this work, you should never access files on the filesystem directly. Instead, use :meth:`raw_data` or :meth:`open` to read/write to component files in the book. When converting between hrefs and names use the methods provided by this class, they assume all hrefs are quoted. ''' #: The type of book (epub for EPUB files and azw3 for AZW3 files) book_type = 'oeb' #: If this container represents an unzipped book (a directory) is_dir = False SUPPORTS_TITLEPAGES = True SUPPORTS_FILENAMES = True @property def book_type_for_display(self): return self.book_type.upper() def __init__(self, rootpath, opfpath, log, clone_data=None): ContainerBase.__init__(self, log) self.root = clone_data['root'] if clone_data is not None else os.path.abspath(rootpath) self.name_path_map = {} self.dirtied = set() self.pretty_print = set() self.cloned = False self.cache_names = ('parsed_cache', 'mime_map', 'name_path_map', 'encoding_map', 'dirtied', 'pretty_print') self.href_to_name_cache = {} if clone_data is not None: self.cloned = True for x in ('name_path_map', 'opf_name', 'mime_map', 'pretty_print', 'encoding_map', 'tweak_mode'): setattr(self, x, clone_data[x]) self.opf_dir = os.path.dirname(self.name_path_map[self.opf_name]) return # Map of relative paths with '/' separators from root of unzipped ePub # to absolute paths on filesystem with os-specific separators opfpath = os.path.abspath(os.path.realpath(opfpath)) all_opf_files = [] for dirpath, _dirnames, filenames in os.walk(self.root): for f in filenames: path = join(dirpath, f) name = self.abspath_to_name(path) self.name_path_map[name] = path self.mime_map[name] = guess_type(path) # Special case if we have stumbled onto the opf if path == opfpath: self.opf_name = name self.opf_dir = os.path.dirname(path) self.mime_map[name] = guess_type('a.opf') if path.lower().endswith('.opf'): all_opf_files.append((name, os.path.dirname(path))) if not hasattr(self, 'opf_name') and all_opf_files: self.opf_name, self.opf_dir = all_opf_files[0] self.mime_map[self.opf_name] = guess_type('a.opf') if not hasattr(self, 'opf_name'): raise InvalidBook('Could not locate opf file: %r'%opfpath) # Update mime map with data from the OPF self.refresh_mime_map() def refresh_mime_map(self): for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'): href = item.get('href') try: name = self.href_to_name(href, self.opf_name) except ValueError: continue # special filenames such as CON on windows cause relpath to fail mt = item.get('media-type') if name in self.mime_map and name != self.opf_name and mt: # some epubs include the opf in the manifest with an incorrect mime type self.mime_map[name] = mt def data_for_clone(self, dest_dir=None): dest_dir = dest_dir or self.root return { 'root': dest_dir, 'opf_name': self.opf_name, 'mime_map': self.mime_map.copy(), 'pretty_print': set(self.pretty_print), 'encoding_map': self.encoding_map.copy(), 'tweak_mode': self.tweak_mode, 'name_path_map': { name:os.path.join(dest_dir, os.path.relpath(path, self.root)) for name, path in iteritems(self.name_path_map)} } def clone_data(self, dest_dir): Container.commit(self, keep_parsed=False) self.cloned = True clone_dir(self.root, dest_dir) return self.data_for_clone(dest_dir)

[documentos] def add_name_to_manifest(self, name, process_manifest_item=None): ' Add an entry to the manifest for a file with the specified name. Returns the manifest id. ' all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')} c = 0 item_id = 'id' while item_id in all_ids: c += 1 item_id = 'id' + '%d'%c manifest = self.opf_xpath('//opf:manifest')[0] href = self.name_to_href(name, self.opf_name) item = manifest.makeelement(OPF('item'), id=item_id, href=href) item.set('media-type', self.mime_map[name]) self.insert_into_xml(manifest, item) if process_manifest_item is not None: process_manifest_item(item) self.dirty(self.opf_name) return item_id

[documentos] def manifest_has_name(self, name): ''' Return True if the manifest has an entry corresponding to name ''' all_names = {self.href_to_name(x.get('href'), self.opf_name) for x in self.opf_xpath('//opf:manifest/opf:item[@href]')} return name in all_names

[documentos] def make_name_unique(self, name): ''' Ensure that `name` does not already exist in this book. If it does, return a modified version that does not exist. ''' counter = count() while self.has_name_case_insensitive(name) or self.manifest_has_name(name): c = next(counter) + 1 base, ext = name.rpartition('.')[::2] if c > 1: base = base.rpartition('-')[0] name = '%s-%d.%s' % (base, c, ext) return name

[documentos] def add_file(self, name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None): ''' Add a file to this container. Entries for the file are automatically created in the OPF manifest and spine (if the file is a text document) ''' if '..' in name: raise ValueError('Names are not allowed to have .. in them') href = self.name_to_href(name, self.opf_name) if self.has_name_case_insensitive(name) or self.manifest_has_name(name): if not modify_name_if_needed: raise ValueError(('A file with the name %s already exists' % name) if self.has_name_case_insensitive(name) else ('An item with the href %s already exists in the manifest' % href)) name = self.make_name_unique(name) href = self.name_to_href(name, self.opf_name) path = self.name_to_abspath(name) base = os.path.dirname(path) if not os.path.exists(base): os.makedirs(base) with open(path, 'wb') as f: if hasattr(data, 'read'): shutil.copyfileobj(data, f) else: f.write(data) mt = media_type or self.guess_type(name) self.name_path_map[name] = path self.mime_map[name] = mt if self.ok_to_be_unmanifested(name): return name item_id = self.add_name_to_manifest(name, process_manifest_item=process_manifest_item) if mt in OEB_DOCS: manifest = self.opf_xpath('//opf:manifest')[0] spine = self.opf_xpath('//opf:spine')[0] si = manifest.makeelement(OPF('itemref'), idref=item_id) self.insert_into_xml(spine, si, index=spine_index) return name

[documentos] def rename(self, current_name, new_name): ''' Renames a file from current_name to new_name. It automatically rebases all links inside the file if the folder the file is in changes. Note however, that links are not updated in the other files that could reference this file. This is for performance, such updates should be done once, in bulk. ''' if current_name in self.names_that_must_not_be_changed: raise ValueError('Renaming of %s is not allowed' % current_name) if self.exists(new_name) and (new_name == current_name or new_name.lower() != current_name.lower()): # The destination exists and does not differ from the current name only by case raise ValueError(f'Cannot rename {current_name} to {new_name} as {new_name} already exists') new_path = self.name_to_abspath(new_name) base = os.path.dirname(new_path) if os.path.isfile(base): raise ValueError(f'Cannot rename {current_name} to {new_name} as {base} is a file') if not os.path.exists(base): os.makedirs(base) old_path = parent_dir = self.name_to_abspath(current_name) self.commit_item(current_name) os.rename(old_path, new_path) # Remove empty directories while parent_dir: parent_dir = os.path.dirname(parent_dir) try: os.rmdir(parent_dir) except OSError: break for x in ('mime_map', 'encoding_map'): x = getattr(self, x) if current_name in x: x[new_name] = x[current_name] self.name_path_map[new_name] = new_path for x in self.cache_names: x = getattr(self, x) try: x.pop(current_name, None) except TypeError: x.discard(current_name) if current_name == self.opf_name: self.opf_name = new_name if os.path.dirname(old_path) != os.path.dirname(new_path): from calibre.ebooks.oeb.polish.replace import LinkRebaser repl = LinkRebaser(self, current_name, new_name) self.replace_links(new_name, repl) self.dirty(new_name)

[documentos] def replace_links(self, name, replace_func): ''' Replace all links in name using replace_func, which must be a callable that accepts a URL and returns the replaced URL. It must also have a 'replaced' attribute that is set to True if any actual replacement is done. Convenient ways of creating such callables are using the :class:`LinkReplacer` and :class:`LinkRebaser` classes. ''' media_type = self.mime_map.get(name, guess_type(name)) if name == self.opf_name: replace_func.file_type = 'opf' for elem in self.opf_xpath('//*[@href]'): elem.set('href', replace_func(elem.get('href'))) elif media_type.lower() in OEB_DOCS: replace_func.file_type = 'text' rewrite_links(self.parsed(name), replace_func) elif media_type.lower() in OEB_STYLES: replace_func.file_type = 'style' replaceUrls(self.parsed(name), replace_func) elif media_type.lower() == guess_type('toc.ncx'): replace_func.file_type = 'ncx' for elem in self.parsed(name).xpath('//*[@src]'): elem.set('src', replace_func(elem.get('src'))) if replace_func.replaced: self.dirty(name) return replace_func.replaced

[documentos] def iterlinks(self, name, get_line_numbers=True): ''' Iterate over all links in name. If get_line_numbers is True the yields results of the form (link, line_number, offset). Where line_number is the line_number at which the link occurs and offset is the number of characters from the start of the line. Note that offset could actually encompass several lines if not zero. ''' media_type = self.mime_map.get(name, guess_type(name)) if name == self.opf_name: for elem in self.opf_xpath('//*[@href]'): yield (elem.get('href'), elem.sourceline, 0) if get_line_numbers else elem.get('href') elif media_type.lower() in OEB_DOCS: for el, attr, link, pos in iterlinks(self.parsed(name)): yield (link, el.sourceline, pos) if get_line_numbers else link elif media_type.lower() in OEB_STYLES: if get_line_numbers: with self.open(name, 'rb') as f: raw = self.decode(f.read()).replace('\r\n', '\n').replace('\r', '\n') position = PositionFinder(raw) is_in_comment = CommentFinder(raw) for link, offset in itercsslinks(raw): if not is_in_comment(offset): lnum, col = position(offset) yield link, lnum, col else: for link in getUrls(self.parsed(name)): yield link elif media_type.lower() == guess_type('toc.ncx'): for elem in self.parsed(name).xpath('//*[@src]'): yield (elem.get('src'), elem.sourceline, 0) if get_line_numbers else elem.get('src')

[documentos] def abspath_to_name(self, fullpath, root=None): ''' Convert an absolute path to a canonical name relative to :attr:`root` :param root: The base folder. By default the root for this container object is used. ''' # OS X silently changes all file names to NFD form. The EPUB # spec requires all text including filenames to be in NFC form. # The proper fix is to implement a VFS that maps between # canonical names and their file system representation, however, # I dont have the time for that now. Note that the container # ensures that all text files are normalized to NFC when # decoding them anyway, so there should be no mismatch between # names in the text and NFC canonical file names. return unicodedata.normalize('NFC', abspath_to_name(fullpath, root or self.root))

[documentos] def name_to_abspath(self, name): ' Convert a canonical name to an absolute OS dependent path ' return name_to_abspath(name, self.root)

[documentos] def exists(self, name): ''' True iff a file/folder corresponding to the canonical name exists. Note that this function suffers from the limitations of the underlying OS filesystem, in particular case (in)sensitivity. So on a case insensitive filesystem this will return True even if the case of name is different from the case of the underlying filesystem file. See also :meth:`has_name`''' return os.path.exists(self.name_to_abspath(name))

[documentos] def href_to_name(self, href, base=None): ''' Convert an href (relative to base) to a name. base must be a name or None, in which case self.root is used. ''' key = href, base ans = self.href_to_name_cache.get(key, null) if ans is null: ans = self.href_to_name_cache[key] = href_to_name(href, self.root, base=base) return ans

[documentos] def name_to_href(self, name, base=None): '''Convert a name to a href relative to base, which must be a name or None in which case self.root is used as the base''' return name_to_href(name, self.root, base=base)

[documentos] def opf_xpath(self, expr): ' Convenience method to evaluate an XPath expression on the OPF file, has the opf: and dc: namespace prefixes pre-defined. ' return self.opf.xpath(expr, namespaces=OPF_NAMESPACES)

[documentos] def has_name(self, name): ''' Return True iff a file with the same canonical name as that specified exists. Unlike :meth:`exists` this method is always case-sensitive. ''' return name and name in self.name_path_map

def has_name_and_is_not_empty(self, name): if not self.has_name(name): return False try: return os.path.getsize(self.name_path_map[name]) > 0 except OSError: return False def has_name_case_insensitive(self, name): if not name: return False name = name.lower() for q in self.name_path_map: if q.lower() == name: return True return False

[documentos] def relpath(self, path, base=None): '''Convert an absolute path (with os separators) to a path relative to base (defaults to self.root). The relative path is *not* a name. Use :meth:`abspath_to_name` for that.''' return relpath(path, base or self.root)

def ok_to_be_unmanifested(self, name): return name in self.names_that_need_not_be_manifested @property def names_that_need_not_be_manifested(self): ' Set of names that are allowed to be missing from the manifest. Depends on the e-book file format. ' return {self.opf_name} @property def names_that_must_not_be_removed(self): ' Set of names that must never be deleted from the container. Depends on the e-book file format. ' return {self.opf_name} @property def names_that_must_not_be_changed(self): ' Set of names that must never be renamed. Depends on the e-book file format. ' return set() def parse(self, path, mime): with open(path, 'rb') as src: data = src.read() if mime in OEB_DOCS: data = self.parse_xhtml(data, self.relpath(path)) elif mime[-4:] in {'+xml', '/xml'}: data = self.parse_xml(data) elif mime in OEB_STYLES: data = self.parse_css(data, self.relpath(path)) return data

[documentos] def raw_data(self, name, decode=True, normalize_to_nfc=True): ''' Return the raw data corresponding to the file specified by name :param decode: If True and the file has a text based MIME type, decode it and return a unicode object instead of raw bytes. :param normalize_to_nfc: If True the returned unicode object is normalized to the NFC normal form as is required for the EPUB and AZW3 file formats. ''' with self.open(name) as nf: ans = nf.read() mime = self.mime_map.get(name, guess_type(name)) if decode and (mime in OEB_STYLES or mime in OEB_DOCS or mime == 'text/plain' or mime[-4:] in {'+xml', '/xml'}): ans = self.decode(ans, normalize_to_nfc=normalize_to_nfc) return ans

[documentos] def parsed(self, name): ''' Return a parsed representation of the file specified by name. For HTML and XML files an lxml tree is returned. For CSS files a css_parser stylesheet is returned. Note that parsed objects are cached for performance. If you make any changes to the parsed object, you must call :meth:`dirty` so that the container knows to update the cache. See also :meth:`replace`.''' ans = self.parsed_cache.get(name, None) if ans is None: self.used_encoding = None mime = self.mime_map.get(name, guess_type(name)) ans = self.parse(self.name_path_map[name], mime) self.parsed_cache[name] = ans self.encoding_map[name] = self.used_encoding return ans

[documentos] def replace(self, name, obj): ''' Replace the parsed object corresponding to name with obj, which must be a similar object, i.e. an lxml tree for HTML/XML or a css_parser stylesheet for a CSS file. ''' self.parsed_cache[name] = obj self.dirty(name)

@property def opf(self): ' The parsed OPF file ' return self.parsed(self.opf_name) @property def mi(self): ''' The metadata of this book as a Metadata object. Note that this object is constructed on the fly every time this property is requested, so use it sparingly. ''' from calibre.ebooks.metadata.opf2 import OPF as O mi = self.serialize_item(self.opf_name) return O(BytesIO(mi), basedir=self.opf_dir, unquote_urls=False, populate_spine=False).to_book_metadata() @property def opf_version(self): ' The version set on the OPF\'s <package> element ' try: return self.opf_xpath('//opf:package/@version')[0] except IndexError: return '' @property def opf_version_parsed(self): ' The version set on the OPF\'s <package> element as a tuple of integers ' return parse_opf_version(self.opf_version) @property def manifest_id_map(self): ' Mapping of manifest id to canonical names ' return {item.get('id'):self.href_to_name(item.get('href'), self.opf_name) for item in self.opf_xpath('//opf:manifest/opf:item[@href and @id]')} @property def manifest_type_map(self): ' Mapping of manifest media-type to list of canonical names of that media-type ' ans = defaultdict(list) for item in self.opf_xpath('//opf:manifest/opf:item[@href and @media-type]'): ans[item.get('media-type').lower()].append(self.href_to_name( item.get('href'), self.opf_name)) return {mt:tuple(v) for mt, v in iteritems(ans)}

[documentos] def manifest_items_with_property(self, property_name): ' All manifest items that have the specified property ' prefixes = read_prefixes(self.opf) for item in items_with_property(self.opf, property_name, prefixes): href = item.get('href') if href: yield self.href_to_name(item.get('href'), self.opf_name)

[documentos] def manifest_items_of_type(self, predicate): ''' The names of all manifest items whose media-type matches predicate. `predicate` can be a set, a list, a string or a function taking a single argument, which will be called with the media-type. ''' if isinstance(predicate, str): predicate = predicate.__eq__ elif hasattr(predicate, '__contains__'): predicate = predicate.__contains__ for mt, names in iteritems(self.manifest_type_map): if predicate(mt): yield from names

[documentos] def apply_unique_properties(self, name, *properties): ''' Ensure that the specified properties are set on only the manifest item identified by name. You can pass None as the name to remove the property from all items. ''' properties = frozenset(properties) removed_names, added_names = [], [] for p in properties: if p.startswith('calibre:'): ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX) break for item in self.opf_xpath('//opf:manifest/opf:item'): iname = self.href_to_name(item.get('href'), self.opf_name) props = (item.get('properties') or '').split() lprops = {p.lower() for p in props} for prop in properties: if prop.lower() in lprops: if name != iname: removed_names.append(iname) props = [p for p in props if p.lower() != prop] if props: item.set('properties', ' '.join(props)) else: del item.attrib['properties'] else: if name == iname: added_names.append(iname) props.append(prop) item.set('properties', ' '.join(props)) self.dirty(self.opf_name) return removed_names, added_names

[documentos] def add_properties(self, name, *properties): ''' Add the specified properties to the manifest item identified by name. ''' properties = frozenset(properties) if not properties: return True for p in properties: if p.startswith('calibre:'): ensure_prefix(self.opf, None, 'calibre', CALIBRE_PREFIX) break for item in self.opf_xpath('//opf:manifest/opf:item'): iname = self.href_to_name(item.get('href'), self.opf_name) if name == iname: props = frozenset((item.get('properties') or '').split()) | properties item.set('properties', ' '.join(props)) return True return False

@property def guide_type_map(self): ' Mapping of guide type to canonical name ' return {item.get('type', ''):self.href_to_name(item.get('href'), self.opf_name) for item in self.opf_xpath('//opf:guide/opf:reference[@href and @type]')} @property def spine_iter(self): ''' An iterator that yields item, name is_linear for every item in the books' spine. item is the lxml element, name is the canonical file name and is_linear is True if the item is linear. See also: :attr:`spine_names` and :attr:`spine_items`. ''' manifest_id_map = self.manifest_id_map non_linear = [] for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'): idref = item.get('idref') name = manifest_id_map.get(idref, None) path = self.name_path_map.get(name, None) if path: if item.get('linear', 'yes') == 'yes': yield item, name, True else: non_linear.append((item, name)) for item, name in non_linear: yield item, name, False def index_in_spine(self, name): manifest_id_map = self.manifest_id_map for i, item in enumerate(self.opf_xpath('//opf:spine/opf:itemref[@idref]')): idref = item.get('idref') q = manifest_id_map.get(idref, None) if q == name: return i @property def spine_names(self): ''' An iterator yielding name and is_linear for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. ''' for item, name, linear in self.spine_iter: yield name, linear @property def spine_items(self): ''' An iterator yielding the path for every item in the books' spine. See also: :attr:`spine_iter` and :attr:`spine_items`. ''' for name, linear in self.spine_names: yield self.name_path_map[name]

[documentos] def remove_from_spine(self, spine_items, remove_if_no_longer_in_spine=True): ''' Remove the specified items (by canonical name) from the spine. If ``remove_if_no_longer_in_spine`` is True, the items are also deleted from the book, not just from the spine. ''' nixed = set() for (name, remove), (item, xname, linear) in zip(spine_items, self.spine_iter): if remove and name == xname: self.remove_from_xml(item) nixed.add(name) if remove_if_no_longer_in_spine: # Remove from the book if no longer in spine nixed -= {name for name, linear in self.spine_names} for name in nixed: self.remove_item(name)

[documentos] def set_spine(self, spine_items): ''' Set the spine to be spine_items where spine_items is an iterable of the form (name, linear). Will raise an error if one of the names is not present in the manifest. ''' imap = self.manifest_id_map imap = {name:item_id for item_id, name in iteritems(imap)} items = [item for item, name, linear in self.spine_iter] tail, last_tail = (items[0].tail, items[-1].tail) if items else ('\n ', '\n ') for i in items: self.remove_from_xml(i) spine = self.opf_xpath('//opf:spine')[0] spine.text = tail for name, linear in spine_items: i = spine.makeelement('{%s}itemref' % OPF_NAMESPACES['opf'], nsmap={'opf':OPF_NAMESPACES['opf']}) i.tail = tail i.set('idref', imap[name]) spine.append(i) if not linear: i.set('linear', 'no') if len(spine) > 0: spine[-1].tail = last_tail self.dirty(self.opf_name)

[documentos] def remove_item(self, name, remove_from_guide=True): ''' Remove the item identified by name from this container. This removes all references to the item in the OPF manifest, guide and spine as well as from any internal caches. ''' removed = set() for elem in self.opf_xpath('//opf:manifest/opf:item[@href]'): if self.href_to_name(elem.get('href'), self.opf_name) == name: id_ = elem.get('id', None) if id_ is not None: removed.add(id_) self.remove_from_xml(elem) self.dirty(self.opf_name) if removed: for spine in self.opf_xpath('//opf:spine'): tocref = spine.attrib.get('toc', None) if tocref and tocref in removed: spine.attrib.pop('toc', None) self.dirty(self.opf_name) for item in self.opf_xpath('//opf:spine/opf:itemref[@idref]'): idref = item.get('idref') if idref in removed: self.remove_from_xml(item) self.dirty(self.opf_name) for meta in self.opf_xpath('//opf:meta[@name="cover" and @content]'): if meta.get('content') in removed: self.remove_from_xml(meta) self.dirty(self.opf_name) if remove_from_guide: for item in self.opf_xpath('//opf:guide/opf:reference[@href]'): if self.href_to_name(item.get('href'), self.opf_name) == name: self.remove_from_xml(item) self.dirty(self.opf_name) path = self.name_path_map.pop(name, None) if path and os.path.exists(path): os.remove(path) self.mime_map.pop(name, None) self.parsed_cache.pop(name, None) self.dirtied.discard(name)

[documentos] def dirty(self, name): ''' Mark the parsed object corresponding to name as dirty. See also: :meth:`parsed`. ''' self.dirtied.add(name)

[documentos] def remove_from_xml(self, item): 'Removes item from parent, fixing indentation (works only with self closing items)' parent = item.getparent() idx = parent.index(item) if idx == 0: # We are removing the first item - only care about adjusting # the tail if this was the only child if len(parent) == 1: parent.text = item.tail else: # Make sure the preceding item has this tail parent[idx-1].tail = item.tail parent.remove(item) return item

[documentos] def insert_into_xml(self, parent, item, index=None): '''Insert item into parent (or append if index is None), fixing indentation. Only works with self closing items.''' if index is None: parent.append(item) else: parent.insert(index, item) idx = parent.index(item) if idx == 0: item.tail = parent.text # If this is the only child of this parent element, we need a # little extra work as we have gone from a self-closing <foo /> # element to <foo><item /></foo> if len(parent) == 1: sibling = parent.getprevious() if sibling is None: # Give up! return parent.text = sibling.text item.tail = sibling.tail else: item.tail = parent[idx-1].tail if idx == len(parent)-1: parent[idx-1].tail = parent.text

[documentos] def opf_get_or_create(self, name): ''' Convenience method to either return the first XML element with the specified name or create it under the opf:package element and then return it, if it does not already exist. ''' ans = self.opf_xpath('//opf:'+name) if ans: return ans[0] self.dirty(self.opf_name) package = self.opf_xpath('//opf:package')[0] item = package.makeelement(OPF(name)) item.tail = '\n' package.append(item) return item

[documentos] def generate_item(self, name, id_prefix=None, media_type=None, unique_href=True): '''Add an item to the manifest with href derived from the given name. Ensures uniqueness of href and id automatically. Returns generated item.''' id_prefix = id_prefix or 'id' media_type = media_type or self.guess_type(name) if unique_href: name = self.make_name_unique(name) href = self.name_to_href(name, self.opf_name) base, ext = href.rpartition('.')[0::2] all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')} c = 0 item_id = id_prefix while item_id in all_ids: c += 1 item_id = id_prefix + '%d'%c manifest = self.opf_xpath('//opf:manifest')[0] item = manifest.makeelement(OPF('item'), id=item_id, href=href) item.set('media-type', media_type) self.insert_into_xml(manifest, item) self.dirty(self.opf_name) name = self.href_to_name(href, self.opf_name) self.name_path_map[name] = path = self.name_to_abspath(name) self.mime_map[name] = media_type # Ensure that the file corresponding to the newly created item exists # otherwise cloned containers will fail when they try to get the number # of links to the file base = os.path.dirname(path) if not os.path.exists(base): os.makedirs(base) open(path, 'wb').close() return item

def format_opf(self): try: mdata = self.opf_xpath('//opf:metadata')[0] except IndexError: pass else: mdata.text = '\n ' remove = set() for child in mdata: child.tail = '\n ' try: if (child.get('name', '').startswith('calibre:' ) and child.get('content', '').strip() in {'{}', ''}): remove.add(child) except AttributeError: continue # Happens for XML comments for child in remove: mdata.remove(child) if len(mdata) > 0: mdata[-1].tail = '\n ' # Ensure name comes before content, needed for Nooks for meta in self.opf_xpath('//opf:meta[@name="cover"]'): if 'content' in meta.attrib: meta.set('content', meta.attrib.pop('content'))

[documentos] def serialize_item(self, name): ''' Convert a parsed object (identified by canonical name) into a bytestring. See :meth:`parsed`. ''' data = root = self.parsed(name) if name == self.opf_name: self.format_opf() data = serialize(data, self.mime_map[name], pretty_print=name in self.pretty_print) if name == self.opf_name and root.nsmap.get(None) == OPF2_NS: # Needed as I can't get lxml to output opf:role and # not output <opf:metadata> as well data = re.sub(br'(<[/]{0,1})opf:', br'\1', data) return data

[documentos] def commit_item(self, name, keep_parsed=False): ''' Commit a parsed object to disk (it is serialized and written to the underlying file). If ``keep_parsed`` is True the parsed representation is retained in the cache. See also: :meth:`parsed` ''' if name not in self.parsed_cache: return data = self.serialize_item(name) self.dirtied.discard(name) if not keep_parsed: self.parsed_cache.pop(name) dest = self.name_path_map[name] if self.cloned and nlinks_file(dest) > 1: # Decouple this file from its links os.unlink(dest) with open(dest, 'wb') as f: f.write(data)

[documentos] def filesize(self, name): ''' Return the size in bytes of the file represented by the specified canonical name. Automatically handles dirtied parsed objects. See also: :meth:`parsed` ''' if name in self.dirtied: self.commit_item(name, keep_parsed=True) path = self.name_to_abspath(name) return os.path.getsize(path)

[documentos] def get_file_path_for_processing(self, name, allow_modification=True): ''' Similar to open() except that it returns a file path, instead of an open file object. ''' if name in self.dirtied: self.commit_item(name) self.parsed_cache.pop(name, False) path = self.name_to_abspath(name) base = os.path.dirname(path) if not os.path.exists(base): os.makedirs(base) else: if self.cloned and allow_modification and os.path.exists(path) and nlinks_file(path) > 1: # Decouple this file from its links temp = path + 'xxx' shutil.copyfile(path, temp) if iswindows: retry_on_fail(os.unlink, path) else: os.unlink(path) os.rename(temp, path) return path

[documentos] def open(self, name, mode='rb'): ''' Open the file pointed to by name for direct read/write. Note that this will commit the file if it is dirtied and remove it from the parse cache. You must finish with this file before accessing the parsed version of it again, or bad things will happen. ''' return open(self.get_file_path_for_processing(name, mode not in {'r', 'rb'}), mode)

[documentos] def commit(self, outpath=None, keep_parsed=False): ''' Commit all dirtied parsed objects to the filesystem and write out the e-book file at outpath. :param output: The path to write the saved e-book file to. If None, the path of the original book file is used. :param keep_parsed: If True the parsed representations of committed items are kept in the cache. ''' for name in tuple(self.dirtied): self.commit_item(name, keep_parsed=keep_parsed)

def compare_to(self, other): if set(self.name_path_map) != set(other.name_path_map): return 'Set of files is not the same' mismatches = [] for name, path in iteritems(self.name_path_map): opath = other.name_path_map[name] with open(path, 'rb') as f1, open(opath, 'rb') as f2: if f1.read() != f2.read(): mismatches.append('The file %s is not the same'%name) return '\n'.join(mismatches)

# }}}

# EPUB {{{

class InvalidEpub(InvalidBook):
    pass

class ObfuscationKeyMissing(InvalidEpub):
    pass

OCF_NS = 'urn:oasis:names:tc:opendocument:xmlns:container'
VCS_IGNORE_FILES = frozenset('.gitignore .hgignore .agignore .bzrignore'.split())
VCS_DIRS = frozenset(('.git', '.hg', '.svn', '.bzr'))

def walk_dir(basedir):
    for dirpath, dirnames, filenames in os.walk(basedir):
        for vcsdir in VCS_DIRS:
            try:
                dirnames.remove(vcsdir)
            except Exception:
                pass
        is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(basedir))
        yield is_root, dirpath, None
        for fname in filenames:
            if fname not in VCS_IGNORE_FILES:
                yield is_root, dirpath, fname

class EpubContainer(Container):

    book_type = 'epub'

    @property
    def book_type_for_display(self):
        ans = self.book_type.upper()
        try:
            v = self.opf_version_parsed
        except Exception:
            pass
        else:
            try:
                if v.major == 2:
                    ans += ' 2'
                else:
                    if not v.minor:
                        ans += f' {v.major}'
                    else:
                        ans += f' {v.major}.{v.minor}'
            except Exception:
                pass
        return ans

    META_INF = {
            'container.xml': True,
            'manifest.xml': False,
            'encryption.xml': False,
            'metadata.xml': False,
            'signatures.xml': False,
            'rights.xml': False,
    }

    def __init__(self, pathtoepub, log, clone_data=None, tdir=None):
        if clone_data is not None:
            super().__init__(None, None, log, clone_data=clone_data)
            for x in ('pathtoepub', 'obfuscated_fonts', 'is_dir'):
                setattr(self, x, clone_data[x])
            return

        self.pathtoepub = pathtoepub
        if tdir is None:
            tdir = PersistentTemporaryDirectory('_epub_container')
        tdir = os.path.abspath(os.path.realpath(tdir))
        self.root = tdir
        self.is_dir = os.path.isdir(pathtoepub)
        if self.is_dir:
            for is_root, dirpath, fname in walk_dir(self.pathtoepub):
                if is_root:
                    base = tdir
                else:
                    base = os.path.join(tdir, os.path.relpath(dirpath, self.pathtoepub))
                    if fname is None:
                        os.mkdir(base)
                if fname is not None:
                    shutil.copy(os.path.join(dirpath, fname), os.path.join(base, fname))
        else:
            with open(self.pathtoepub, 'rb') as stream:
                try:
                    zf = ZipFile(stream)
                    zf.extractall(tdir)
                except:
                    log.exception('EPUB appears to be invalid ZIP file, trying a'
                            ' more forgiving ZIP parser')
                    from calibre.utils.localunzip import extractall
                    stream.seek(0)
                    extractall(stream, path=tdir)
        try:
            os.remove(join(tdir, 'mimetype'))
        except OSError:
            pass
        # Ensure all filenames are in NFC normalized form
        # has no effect on HFS+ filesystems as they always store filenames
        # in NFD form
        for filename in walk(self.root):
            n = unicodedata.normalize('NFC', filename)
            if n != filename:
                s = filename + 'suff1x'
                os.rename(filename, s)
                os.rename(s, n)

        container_path = join(self.root, 'META-INF', 'container.xml')
        if not exists(container_path):
            raise InvalidEpub('No META-INF/container.xml in epub')
        with open(container_path, 'rb') as cf:
            container = safe_xml_fromstring(cf.read())
        opf_files = container.xpath((
            r'child::ocf:rootfiles/ocf:rootfile'
            '[@media-type="%s" and @full-path]'%guess_type('a.opf')
            ), namespaces={'ocf':OCF_NS}
        )
        if not opf_files:
            raise InvalidEpub('META-INF/container.xml contains no link to OPF file')
        opf_path = os.path.join(self.root, *(urlunquote(opf_files[0].get('full-path')).split('/')))
        if not exists(opf_path):
            raise InvalidEpub('OPF file does not exist at location pointed to'
                    ' by META-INF/container.xml')

        super().__init__(tdir, opf_path, log)

        self.obfuscated_fonts = {}
        if 'META-INF/encryption.xml' in self.name_path_map:
            self.process_encryption()
        self.parsed_cache['META-INF/container.xml'] = container

    def clone_data(self, dest_dir):
        ans = super().clone_data(dest_dir)
        ans['pathtoepub'] = self.pathtoepub
        ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
        ans['is_dir'] = self.is_dir
        return ans

    def rename(self, old_name, new_name):
        is_opf = old_name == self.opf_name
        super().rename(old_name, new_name)
        if is_opf:
            for elem in self.parsed('META-INF/container.xml').xpath((
                r'child::ocf:rootfiles/ocf:rootfile'
                '[@media-type="%s" and @full-path]'%guess_type('a.opf')
                ), namespaces={'ocf':OCF_NS}
            ):
                # The asinine epubcheck cannot handle quoted filenames in
                # container.xml
                elem.set('full-path', self.opf_name)
            self.dirty('META-INF/container.xml')
        if old_name in self.obfuscated_fonts:
            self.obfuscated_fonts[new_name] = self.obfuscated_fonts.pop(old_name)
            enc = self.parsed('META-INF/encryption.xml')
            for cr in enc.xpath('//*[local-name()="CipherReference" and @URI]'):
                if self.href_to_name(cr.get('URI')) == old_name:
                    cr.set('URI', self.name_to_href(new_name))
                    self.dirty('META-INF/encryption.xml')

    @property
    def names_that_need_not_be_manifested(self):
        return super().names_that_need_not_be_manifested | {'META-INF/' + x for x in self.META_INF}

    def ok_to_be_unmanifested(self, name):
        return name in self.names_that_need_not_be_manifested or name.startswith('META-INF/')

    @property
    def names_that_must_not_be_removed(self):
        return super().names_that_must_not_be_removed | {'META-INF/container.xml'}

    @property
    def names_that_must_not_be_changed(self):
        return super().names_that_must_not_be_changed | {'META-INF/' + x for x in self.META_INF}

    def remove_item(self, name, remove_from_guide=True):
        # Handle removal of obfuscated fonts
        if name == 'META-INF/encryption.xml':
            self.obfuscated_fonts.clear()
        if name in self.obfuscated_fonts:
            self.obfuscated_fonts.pop(name, None)
            enc = self.parsed('META-INF/encryption.xml')
            for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
                alg = em.get('Algorithm')
                if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
                    continue
                try:
                    cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
                except (IndexError, ValueError, KeyError):
                    continue
                if name == self.href_to_name(cr.get('URI')):
                    self.remove_from_xml(em.getparent())
                    self.dirty('META-INF/encryption.xml')
        super().remove_item(name, remove_from_guide=remove_from_guide)

    def read_raw_unique_identifier(self):
        package_id = raw_unique_identifier = idpf_key = None
        for attrib, val in iteritems(self.opf.attrib):
            if attrib.endswith('unique-identifier'):
                package_id = val
                break
        if package_id is not None:
            for elem in self.opf_xpath('//*[@id=%s]'%escape_xpath_attr(package_id)):
                if elem.text:
                    raw_unique_identifier = elem.text
                    break
        if raw_unique_identifier is not None:
            idpf_key = raw_unique_identifier
            idpf_key = re.sub('[\u0020\u0009\u000d\u000a]', '', idpf_key)
            idpf_key = hashlib.sha1(idpf_key.encode('utf-8')).digest()
        return package_id, raw_unique_identifier, idpf_key

    def iter_encryption_entries(self):
        if 'META-INF/encryption.xml' in self.name_path_map:
            enc = self.parsed('META-INF/encryption.xml')
            for em in enc.xpath('//*[local-name()="EncryptionMethod" and @Algorithm]'):
                try:
                    cr = em.getparent().xpath('descendant::*[local-name()="CipherReference" and @URI]')[0]
                except Exception:
                    cr = None
                yield em, cr

    def process_encryption(self):
        fonts = {}
        for em, cr in self.iter_encryption_entries():
            alg = em.get('Algorithm')
            if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
                raise DRMError()
            if cr is None:
                continue
            name = self.href_to_name(cr.get('URI'))
            path = self.name_path_map.get(name, None)
            if path is not None:
                fonts[name] = alg

        package_id, raw_unique_identifier, idpf_key = self.read_raw_unique_identifier()
        key = None
        for item in self.opf_xpath('//*[local-name()="metadata"]/*'
                                   '[local-name()="identifier"]'):
            scheme = None
            for xkey in item.attrib.keys():
                if xkey.endswith('scheme'):
                    scheme = item.get(xkey)
            if (scheme and scheme.lower() == 'uuid') or \
                    (item.text and item.text.startswith('urn:uuid:')):
                try:
                    key = item.text.rpartition(':')[-1]
                    key = uuid.UUID(key).bytes
                except Exception:
                    self.log.exception('Failed to parse obfuscation key')
                    key = None

        for font, alg in iteritems(fonts):
            tkey = key if alg == ADOBE_OBFUSCATION else idpf_key
            if not tkey:
                raise ObfuscationKeyMissing('Failed to find obfuscation key')
            raw = self.raw_data(font, decode=False)
            raw = decrypt_font_data(tkey, raw, alg)
            with self.open(font, 'wb') as f:
                f.write(raw)
            self.obfuscated_fonts[font] = (alg, tkey)

    def update_modified_timestamp(self):
        from calibre.ebooks.metadata.opf3 import set_last_modified_in_opf
        set_last_modified_in_opf(self.opf)
        self.dirty(self.opf_name)

    def commit(self, outpath=None, keep_parsed=False):
        if self.opf_version_parsed.major == 3:
            self.update_modified_timestamp()
        super().commit(keep_parsed=keep_parsed)
        container_path = join(self.root, 'META-INF', 'container.xml')
        if not exists(container_path):
            raise InvalidEpub('No META-INF/container.xml in EPUB, this typically happens if the temporary files calibre'
                              ' is using are deleted by some other program while calibre is running')
        restore_fonts = {}
        for name in self.obfuscated_fonts:
            if name not in self.name_path_map:
                continue
            alg, key = self.obfuscated_fonts[name]
            # Decrypting and encrypting are the same operation (XOR with key)
            restore_fonts[name] = data = self.raw_data(name, decode=False)
            with self.open(name, 'wb') as f:
                f.write(decrypt_font_data(key, data, alg))
        if outpath is None:
            outpath = self.pathtoepub
        if self.is_dir:
            # First remove items from the source dir that do not exist any more
            for is_root, dirpath, fname in walk_dir(self.pathtoepub):
                if fname is not None:
                    if is_root and fname == 'mimetype':
                        continue
                    base = self.root if is_root else os.path.join(self.root, os.path.relpath(dirpath, self.pathtoepub))
                    fpath = os.path.join(base, fname)
                    if not os.path.exists(fpath):
                        os.remove(os.path.join(dirpath, fname))
                        try:
                            os.rmdir(dirpath)
                        except OSError as err:
                            if err.errno != errno.ENOTEMPTY:
                                raise
            # Now copy over everything from root to source dir
            for dirpath, dirnames, filenames in os.walk(self.root):
                is_root = os.path.abspath(os.path.normcase(dirpath)) == os.path.abspath(os.path.normcase(self.root))
                base = self.pathtoepub if is_root else os.path.join(self.pathtoepub, os.path.relpath(dirpath, self.root))
                try:
                    os.mkdir(base)
                except OSError as err:
                    if err.errno != errno.EEXIST:
                        raise
                for fname in filenames:
                    with open(os.path.join(dirpath, fname), 'rb') as src, open(os.path.join(base, fname), 'wb') as dest:
                        shutil.copyfileobj(src, dest)

        else:
            from calibre.ebooks.tweak import zip_rebuilder
            with open(join(self.root, 'mimetype'), 'wb') as f:
                et = guess_type('a.epub')
                if not isinstance(et, bytes):
                    et = et.encode('ascii')
                f.write(et)
            zip_rebuilder(self.root, outpath)
            for name, data in iteritems(restore_fonts):
                with self.open(name, 'wb') as f:
                    f.write(data)

    @property
    def path_to_ebook(self):
        return self.pathtoepub

    @path_to_ebook.setter
    def path_to_ebook(self, val):
        self.pathtoepub = val

# }}}

# AZW3 {{{

class InvalidMobi(InvalidBook):
    pass

def do_explode(path, dest):
    from calibre.ebooks.mobi.reader.mobi6 import MobiReader
    from calibre.ebooks.mobi.reader.mobi8 import Mobi8Reader
    with open(path, 'rb') as stream:
        mr = MobiReader(stream, default_log, None, None)

        with CurrentDir(dest):
            mr = Mobi8Reader(mr, default_log, for_tweak=True)
            opf = os.path.abspath(mr())
            obfuscated_fonts = mr.encrypted_fonts

    return opf, obfuscated_fonts

def opf_to_azw3(opf, outpath, container):
    from calibre.ebooks.conversion.plumber import Plumber, create_oebbook
    from calibre.ebooks.mobi.tweak import set_cover

    class Item(Manifest.Item):

        def _parse_css(self, data):
            # The default CSS parser used by oeb.base inserts the h namespace
            # and resolves all @import rules. We dont want that.
            return container.parse_css(data)

    def specialize(oeb):
        oeb.manifest.Item = Item

    plumber = Plumber(opf, outpath, container.log)
    plumber.setup_options()
    inp = plugin_for_input_format('azw3')
    outp = plugin_for_output_format('azw3')
    plumber.opts.mobi_passthrough = True
    plumber.opts.keep_ligatures = True
    oeb = create_oebbook(container.log, opf, plumber.opts, specialize=specialize)
    set_cover(oeb)
    outp.convert(oeb, outpath, inp, plumber.opts, container.log)

def epub_to_azw3(epub, outpath=None):
    container = get_container(epub, tweak_mode=True)
    changed = False
    for item in container.opf_xpath('//opf:manifest/opf:item[@properties and @href]'):
        p = item.get('properties').split()
        if 'cover-image' in p:
            href = item.get('href')
            guides = container.opf_xpath('//opf:guide')
            if not guides:
                guides = (container.opf.makeelement(OPF('guide')),)
                container.opf.append(guides[0])
            for guide in guides:
                for child in guide:
                    if child.get('type') == 'cover':
                        break
                else:
                    guide.append(guide.makeelement(OPF('reference'), type='cover', href=href))
                    changed = True
            break
        elif 'calibre:title-page' in p:
            item.getparent().remove(item)
    if changed:
        container.dirty(container.opf_name)
        container.commit_item(container.opf_name)
    outpath = outpath or (epub.rpartition('.')[0] + '.azw3')
    opf_to_azw3(container.name_to_abspath(container.opf_name), outpath, container)

class AZW3Container(Container):

    book_type = 'azw3'
    SUPPORTS_TITLEPAGES = False
    SUPPORTS_FILENAMES = False

    def __init__(self, pathtoazw3, log, clone_data=None, tdir=None):
        if clone_data is not None:
            super().__init__(None, None, log, clone_data=clone_data)
            for x in ('pathtoazw3', 'obfuscated_fonts'):
                setattr(self, x, clone_data[x])
            return

        self.pathtoazw3 = pathtoazw3
        if tdir is None:
            tdir = PersistentTemporaryDirectory('_azw3_container')
        tdir = os.path.abspath(os.path.realpath(tdir))
        self.root = tdir
        with open(pathtoazw3, 'rb') as stream:
            raw = stream.read(3)
            if raw == b'TPZ':
                raise InvalidMobi(_('This is not a MOBI file. It is a Topaz file.'))

            try:
                header = MetadataHeader(stream, default_log)
            except MobiError:
                raise InvalidMobi(_('This is not a MOBI file.'))

            if header.encryption_type != 0:
                raise DRMError()

            kf8_type = header.kf8_type

            if kf8_type is None:
                raise InvalidMobi(_('This MOBI file does not contain a KF8 format '
                        'book. KF8 is the new format from Amazon. calibre can '
                        'only edit MOBI files that contain KF8 books. Older '
                        'MOBI files without KF8 are not editable.'))

            if kf8_type == 'joint':
                raise InvalidMobi(_('This MOBI file contains both KF8 and '
                    'older Mobi6 data. calibre can only edit MOBI files '
                    'that contain only KF8 data.'))

        try:
            opf_path, obfuscated_fonts = fork_job(
            'calibre.ebooks.oeb.polish.container', 'do_explode',
            args=(pathtoazw3, tdir), no_output=True)['result']
        except WorkerError as e:
            log(e.orig_tb)
            raise InvalidMobi('Failed to explode MOBI')
        super().__init__(tdir, opf_path, log)
        self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}

    def clone_data(self, dest_dir):
        ans = super().clone_data(dest_dir)
        ans['pathtoazw3'] = self.pathtoazw3
        ans['obfuscated_fonts'] = self.obfuscated_fonts.copy()
        return ans

    def commit(self, outpath=None, keep_parsed=False):
        super().commit(keep_parsed=keep_parsed)
        if outpath is None:
            outpath = self.pathtoazw3
        opf_to_azw3(self.name_path_map[self.opf_name], outpath, self)

    @property
    def path_to_ebook(self):
        return self.pathtoazw3

    @path_to_ebook.setter
    def path_to_ebook(self, val):
        self.pathtoazw3 = val

    @property
    def names_that_must_not_be_changed(self):
        return set(self.name_path_map)
# }}}

def get_container(path, log=None, tdir=None, tweak_mode=False):
    if log is None:
        log = default_log
    try:
        isdir = os.path.isdir(path)
    except Exception:
        isdir = False
    own_tdir = not tdir
    ebook_cls = (AZW3Container if path.rpartition('.')[-1].lower() in {'azw3', 'mobi', 'original_azw3', 'original_mobi'} and not isdir
            else EpubContainer)
    if own_tdir:
        tdir = PersistentTemporaryDirectory(f'_{ebook_cls.book_type}_container')
    try:
        ebook = ebook_cls(path, log, tdir=tdir)
        ebook.tweak_mode = tweak_mode
    except BaseException:
        if own_tdir:
            shutil.rmtree(tdir, ignore_errors=True)
        raise
    return ebook

def test_roundtrip():
    ebook = get_container(sys.argv[-1])
    p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1])
    p.close()
    ebook.commit(outpath=p.name)
    ebook2 = get_container(p.name)
    ebook3 = get_container(p.name)
    diff = ebook3.compare_to(ebook2)
    if diff is not None:
        print(diff)

if __name__ == '__main__':
    test_roundtrip()

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.cover

Código fuente para calibre.ebooks.oeb.polish.cover

__license__   = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import os
import re
import shutil

from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XLINK, XPath, xml2text
from calibre.ebooks.oeb.polish.replace import get_recommended_folders, replace_links
from calibre.utils.imghdr import identify
from polyglot.builtins import iteritems

def set_azw3_cover(container, cover_path, report, options=None):
    existing_image = options is not None and options.get('existing_image', False)
    name = None
    found = True
    for gi in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
        href = gi.get('href')
        name = container.href_to_name(href, container.opf_name)
        container.remove_from_xml(gi)
    if existing_image:
        name = cover_path
        found = False
    else:
        if name is None or not container.has_name(name):
            item = container.generate_item(name='cover.jpeg', id_prefix='cover')
            name = container.href_to_name(item.get('href'), container.opf_name)
            found = False
    href = container.name_to_href(name, container.opf_name)
    guide = container.opf_xpath('//opf:guide')[0]
    container.insert_into_xml(guide, guide.makeelement(
        OPF('reference'), href=href, type='cover'))
    if not existing_image:
        with open(cover_path, 'rb') as src, container.open(name, 'wb') as dest:
            shutil.copyfileobj(src, dest)
    container.dirty(container.opf_name)
    report(_('Cover updated') if found else _('Cover inserted'))

def get_azw3_raster_cover_name(container):
    items = container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]')
    if items:
        return container.href_to_name(items[0].get('href'))

def mark_as_cover_azw3(container, name):
    href = container.name_to_href(name, container.opf_name)
    found = False
    for item in container.opf_xpath('//opf:guide/opf:reference[@href and contains(@type, "cover")]'):
        item.set('href', href)
        found = True
    if not found:
        for guide in container.opf_xpath('//opf:guide'):
            container.insert_into_xml(guide, guide.makeelement(
                OPF('reference'), href=href, type='cover'))
    container.dirty(container.opf_name)

def get_raster_cover_name(container):
    if container.book_type == 'azw3':
        return get_azw3_raster_cover_name(container)
    return find_cover_image(container, strict=True)

def get_cover_page_name(container):
    if container.book_type == 'azw3':
        return
    return find_cover_page(container)

[documentos] def set_cover(container, cover_path, report=None, options=None): ''' Set the cover of the book to the image pointed to by cover_path. :param cover_path: Either the absolute path to an image file or the canonical name of an image in the book. When using an image in the book, you must also set options, see below. :param report: An optional callable that takes a single argument. It will be called with information about the tasks being processed. :param options: None or a dictionary that controls how the cover is set. The dictionary can have entries: **keep_aspect**: True or False (Preserve aspect ratio of covers in EPUB) **no_svg**: True or False (Use an SVG cover wrapper in the EPUB titlepage) **existing**: True or False (``cover_path`` refers to an existing image in the book) ''' report = report or (lambda x:x) if container.book_type == 'azw3': set_azw3_cover(container, cover_path, report, options=options) else: set_epub_cover(container, cover_path, report, options=options)

[documentos] def mark_as_cover(container, name): ''' Mark the specified image as the cover image. ''' if name not in container.mime_map: raise ValueError('Cannot mark %s as cover as it does not exist' % name) mt = container.mime_map[name] if not is_raster_image(mt): raise ValueError('Cannot mark %s as the cover image as it is not a raster image' % name) if container.book_type == 'azw3': mark_as_cover_azw3(container, name) else: mark_as_cover_epub(container, name)

###############################################################################
# The delightful EPUB cover processing

def is_raster_image(media_type):
    return media_type and media_type.lower() in {
        'image/png', 'image/jpeg', 'image/jpg', 'image/gif'}

COVER_TYPES = {
    'coverimagestandard', 'other.ms-coverimage-standard',
    'other.ms-titleimage-standard', 'other.ms-titleimage',
    'other.ms-coverimage', 'other.ms-thumbimage-standard',
    'other.ms-thumbimage', 'thumbimagestandard', 'cover'}

def find_cover_image2(container, strict=False):
    manifest_id_map = container.manifest_id_map
    mm = container.mime_map
    for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
        item_id = meta.get('content')
        name = manifest_id_map.get(item_id, None)
        media_type = mm.get(name, None)
        if is_raster_image(media_type):
            return name

    # First look for a guide item with type == 'cover'
    guide_type_map = container.guide_type_map
    for ref_type, name in iteritems(guide_type_map):
        if ref_type.lower() == 'cover' and is_raster_image(mm.get(name, None)):
            return name

    if strict:
        return

    # Find the largest image from all possible guide cover items
    largest_cover = (None, 0)
    for ref_type, name in iteritems(guide_type_map):
        if ref_type.lower() in COVER_TYPES and is_raster_image(mm.get(name, None)):
            path = container.name_path_map.get(name, None)
            if path:
                sz = os.path.getsize(path)
                if sz > largest_cover[1]:
                    largest_cover = (name, sz)

    if largest_cover[0]:
        return largest_cover[0]

def find_cover_image3(container):
    for name in container.manifest_items_with_property('cover-image'):
        return name
    manifest_id_map = container.manifest_id_map
    mm = container.mime_map
    for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
        item_id = meta.get('content')
        name = manifest_id_map.get(item_id, None)
        media_type = mm.get(name, None)
        if is_raster_image(media_type):
            return name

def find_cover_image(container, strict=False):
    'Find a raster image marked as a cover in the OPF'
    ver = container.opf_version_parsed
    if ver.major < 3:
        return find_cover_image2(container, strict=strict)
    else:
        return find_cover_image3(container)

def get_guides(container):
    guides = container.opf_xpath('//opf:guide')
    if not guides:
        container.insert_into_xml(container.opf, container.opf.makeelement(
            OPF('guide')))
        guides = container.opf_xpath('//opf:guide')
    return guides

def mark_as_cover_epub(container, name):
    mmap = {v:k for k, v in iteritems(container.manifest_id_map)}
    if name not in mmap:
        raise ValueError('Cannot mark %s as cover as it is not in manifest' % name)
    mid = mmap[name]
    ver = container.opf_version_parsed

    # Remove all entries from the opf that identify a raster image as cover
    for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
        container.remove_from_xml(meta)
    for ref in container.opf_xpath('//opf:guide/opf:reference[@href and @type]'):
        if ref.get('type').lower() not in COVER_TYPES:
            continue
        rname = container.href_to_name(ref.get('href'), container.opf_name)
        mt = container.mime_map.get(rname, None)
        if is_raster_image(mt):
            container.remove_from_xml(ref)

    if ver.major < 3:
        # Add reference to image in <metadata>
        for metadata in container.opf_xpath('//opf:metadata'):
            m = metadata.makeelement(OPF('meta'), name='cover', content=mid)
            container.insert_into_xml(metadata, m)

        # If no entry for cover exists in guide, insert one that points to this
        # image
        if not container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'):
            for guide in get_guides(container):
                container.insert_into_xml(guide, guide.makeelement(
                    OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name)))
    else:
        container.apply_unique_properties(name, 'cover-image')

    container.dirty(container.opf_name)

[documentos] def mark_as_titlepage(container, name, move_to_start=True): ''' Mark the specified HTML file as the titlepage of the EPUB. :param move_to_start: If True the HTML file is moved to the start of the spine ''' ver = container.opf_version_parsed if move_to_start: for item, q, linear in container.spine_iter: if name == q: break if not linear: item.set('linear', 'yes') if item.getparent().index(item) > 0: container.insert_into_xml(item.getparent(), item, 0) if ver.major < 3: for ref in container.opf_xpath('//opf:guide/opf:reference[@type="cover"]'): ref.getparent().remove(ref) for guide in get_guides(container): container.insert_into_xml(guide, guide.makeelement( OPF('reference'), type='cover', href=container.name_to_href(name, container.opf_name))) else: container.apply_unique_properties(name, 'calibre:title-page') container.dirty(container.opf_name)

def find_cover_page(container):
    'Find a document marked as a cover in the OPF'
    ver = container.opf_version_parsed
    mm = container.mime_map
    if ver.major < 3:
        guide_type_map = container.guide_type_map
        for ref_type, name in iteritems(guide_type_map):
            if ref_type.lower() == 'cover' and mm.get(name, '').lower() in OEB_DOCS:
                return name
    else:
        for name in container.manifest_items_with_property('calibre:title-page'):
            return name
        from calibre.ebooks.oeb.polish.toc import get_landmarks
        for landmark in get_landmarks(container):
            if landmark['type'] == 'cover' and mm.get(landmark['dest'], '').lower() in OEB_DOCS:
                return landmark['dest']

def fix_conversion_titlepage_links_in_nav(container):
    from calibre.ebooks.oeb.polish.toc import find_existing_nav_toc
    cover_page_name = find_cover_page(container)
    if not cover_page_name:
        return
    nav_page_name = find_existing_nav_toc(container)
    if not nav_page_name:
        return
    for elem in container.parsed(nav_page_name).xpath('//*[@data-calibre-removed-titlepage]'):
        elem.attrib.pop('data-calibre-removed-titlepage')
        elem.set('href', container.name_to_href(cover_page_name, nav_page_name))
    container.dirty(nav_page_name)

def find_cover_image_in_page(container, cover_page):
    root = container.parsed(cover_page)
    body = XPath('//h:body')(root)
    if len(body) != 1:
        return
    body = body[0]
    images = []
    for img in XPath('descendant::h:img[@src]|descendant::svg:svg/descendant::svg:image')(body):
        href = img.get('src') or img.get(XLINK('href'))
        if href:
            name = container.href_to_name(href, base=cover_page)
            images.append(name)
    text = re.sub(r'\s+', '', xml2text(body))
    if text or len(images) > 1:
        # Document has more content than a single image
        return
    if images:
        return images[0]

def clean_opf(container):
    'Remove all references to covers from the OPF'
    manifest_id_map = container.manifest_id_map
    for meta in container.opf_xpath('//opf:meta[@name="cover" and @content]'):
        name = manifest_id_map.get(meta.get('content', None), None)
        container.remove_from_xml(meta)
        if name and name in container.name_path_map:
            yield name

    gtm = container.guide_type_map
    for ref in container.opf_xpath('//opf:guide/opf:reference[@type]'):
        typ = ref.get('type', '')
        if typ.lower() in COVER_TYPES:
            container.remove_from_xml(ref)
            name = gtm.get(typ, None)
            if name and name in container.name_path_map:
                yield name
    ver = container.opf_version_parsed
    if ver.major > 2:
        removed_names = container.apply_unique_properties(None, 'cover-image', 'calibre:title-page')[0]
        for name in removed_names:
            yield name
    container.dirty(container.opf_name)

def create_epub_cover(container, cover_path, existing_image, options=None):
    from calibre.ebooks.conversion.config import load_defaults
    from calibre.ebooks.oeb.transforms.cover import CoverManager

    try:
        ext = cover_path.rpartition('.')[-1].lower()
    except Exception:
        ext = 'jpeg'
    cname, tname = 'cover.' + ext, 'titlepage.xhtml'
    recommended_folders = get_recommended_folders(container, (cname, tname))

    if existing_image:
        raster_cover = existing_image
        manifest_id = {v:k for k, v in iteritems(container.manifest_id_map)}[existing_image]
        raster_cover_item = container.opf_xpath('//opf:manifest/*[@id="%s"]' % manifest_id)[0]
    else:
        folder = recommended_folders[cname]
        if folder:
            cname = folder + '/' + cname
        raster_cover_item = container.generate_item(cname, id_prefix='cover')
        raster_cover = container.href_to_name(raster_cover_item.get('href'), container.opf_name)

        with container.open(raster_cover, 'wb') as dest:
            if callable(cover_path):
                cover_path('write_image', dest)
            else:
                with open(cover_path, 'rb') as src:
                    shutil.copyfileobj(src, dest)
    if options is None:
        opts = load_defaults('epub_output')
        keep_aspect = opts.get('preserve_cover_aspect_ratio', False)
        no_svg = opts.get('no_svg_cover', False)
    else:
        keep_aspect = options.get('keep_aspect', False)
        no_svg = options.get('no_svg', False)
    if no_svg:
        style = 'style="height: 100%%"'
        templ = CoverManager.NONSVG_TEMPLATE.replace('__style__', style)
        has_svg = False
    else:
        if callable(cover_path):
            templ = (options or {}).get('template', CoverManager.SVG_TEMPLATE)
            has_svg = 'xlink:href' in templ
        else:
            width, height = 600, 800
            has_svg = True
            try:
                if existing_image:
                    width, height = identify(container.raw_data(existing_image, decode=False))[1:]
                else:
                    with open(cover_path, 'rb') as csrc:
                        width, height = identify(csrc)[1:]
            except:
                container.log.exception("Failed to get width and height of cover")
            ar = 'xMidYMid meet' if keep_aspect else 'none'
            templ = CoverManager.SVG_TEMPLATE.replace('__ar__', ar)
            templ = templ.replace('__viewbox__', '0 0 %d %d'%(width, height))
            templ = templ.replace('__width__', str(width))
            templ = templ.replace('__height__', str(height))
    folder = recommended_folders[tname]
    if folder:
        tname = folder + '/' + tname
    titlepage_item = container.generate_item(tname, id_prefix='titlepage')
    titlepage = container.href_to_name(titlepage_item.get('href'),
                                          container.opf_name)
    raw = templ % container.name_to_href(raster_cover, titlepage)
    with container.open(titlepage, 'wb') as f:
        if not isinstance(raw, bytes):
            raw = raw.encode('utf-8')
        f.write(raw)

    # We have to make sure the raster cover item has id="cover" for the moron
    # that wrote the Nook firmware
    if raster_cover_item.get('id') != 'cover':
        from calibre.ebooks.oeb.base import uuid_id
        newid = uuid_id()
        for item in container.opf_xpath('//*[@id="cover"]'):
            item.set('id', newid)
        for item in container.opf_xpath('//*[@idref="cover"]'):
            item.set('idref', newid)
        raster_cover_item.set('id', 'cover')

    spine = container.opf_xpath('//opf:spine')[0]
    ref = spine.makeelement(OPF('itemref'), idref=titlepage_item.get('id'))
    container.insert_into_xml(spine, ref, index=0)
    ver = container.opf_version_parsed
    if ver.major < 3:
        guide = container.opf_get_or_create('guide')
        container.insert_into_xml(guide, guide.makeelement(
            OPF('reference'), type='cover', title=_('Cover'),
            href=container.name_to_href(titlepage, base=container.opf_name)))
        metadata = container.opf_get_or_create('metadata')
        meta = metadata.makeelement(OPF('meta'), name='cover')
        meta.set('content', raster_cover_item.get('id'))
        container.insert_into_xml(metadata, meta)
    else:
        container.apply_unique_properties(raster_cover, 'cover-image')
        container.apply_unique_properties(titlepage, 'calibre:title-page')
        if has_svg:
            container.add_properties(titlepage, 'svg')

    return raster_cover, titlepage

def remove_cover_image_in_page(container, page, cover_images):
    for img in container.parsed(page).xpath('//*[local-name()="img" and @src]'):
        href = img.get('src')
        name = container.href_to_name(href, page)
        if name in cover_images:
            img.getparent().remove(img)
        break

def has_epub_cover(container):
    if find_cover_image(container):
        return True
    if find_cover_page(container):
        return True
    spine_items = tuple(container.spine_items)
    if spine_items:
        candidate = container.abspath_to_name(spine_items[0])
        if find_cover_image_in_page(container, candidate) is not None:
            return True
    return False

def set_epub_cover(container, cover_path, report, options=None, image_callback=None):
    existing_image = options is not None and options.get('existing_image', False)
    if existing_image:
        existing_image = cover_path
    cover_image = find_cover_image(container)
    cover_page = find_cover_page(container)
    wrapped_image = extra_cover_page = None
    updated = False
    log = container.log

    possible_removals = set(clean_opf(container))
    possible_removals
    # TODO: Handle possible_removals and also iterate over links in the removed
    # pages and handle possibly removing stylesheets referred to by them.

    image_callback_called = False
    spine_items = tuple(container.spine_items)
    if cover_page is None and spine_items:
        # Check if the first item in the spine is a simple cover wrapper
        candidate = container.abspath_to_name(spine_items[0])
        if find_cover_image_in_page(container, candidate) is not None:
            cover_page = candidate

    if cover_page is not None:
        log('Found existing cover page')
        wrapped_image = find_cover_image_in_page(container, cover_page)

        if len(spine_items) > 1:
            # Look for an extra cover page
            c = container.abspath_to_name(spine_items[1])
            if c != cover_page:
                candidate = find_cover_image_in_page(container, c)
                if candidate and candidate in {wrapped_image, cover_image}:
                    log('Found an extra cover page that is a simple wrapper, removing it')
                    # This page has only a single image and that image is the
                    # cover image, remove it.
                    container.remove_item(c)
                    extra_cover_page = c
                    spine_items = spine_items[:1] + spine_items[2:]
                elif candidate is None:
                    # Remove the cover image if it is the first image in this
                    # page
                    remove_cover_image_in_page(container, c, {wrapped_image,
                                                          cover_image})

        if wrapped_image is not None:
            # The cover page is a simple wrapper around a single cover image,
            # we can remove it safely.
            log(f'Existing cover page {cover_page} is a simple wrapper, removing it')
            container.remove_item(cover_page)
            if wrapped_image != existing_image:
                if image_callback is not None and not image_callback_called:
                    image_callback(cover_image, wrapped_image)
                    image_callback_called = True
                container.remove_item(wrapped_image)
            updated = True

    if image_callback is not None and not image_callback_called:
        image_callback_called = True
        image_callback(cover_image, wrapped_image)
    if cover_image and cover_image != wrapped_image:
        # Remove the old cover image
        if cover_image != existing_image:
            container.remove_item(cover_image)

    # Insert the new cover
    raster_cover, titlepage = create_epub_cover(container, cover_path, existing_image, options=options)

    report(_('Cover updated') if updated else _('Cover inserted'))

    # Replace links to the old cover image/cover page
    link_sub = {s:d for s, d in iteritems({
        cover_page:titlepage, wrapped_image:raster_cover,
        cover_image:raster_cover, extra_cover_page:titlepage})
        if s is not None and s != d}
    if link_sub:
        replace_links(container, link_sub, frag_map=lambda x, y:None)
    return raster_cover, titlepage

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.css

Código fuente para calibre.ebooks.oeb.polish.css

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import re
from collections import defaultdict
from functools import partial
from operator import itemgetter

from css_parser.css import CSSRule, CSSStyleDeclaration
from css_selectors import Select, SelectorError, SelectorSyntaxError, parse

from calibre import force_unicode
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, css_text
from calibre.ebooks.oeb.normalize_css import normalize_filter_css, normalizers
from calibre.ebooks.oeb.polish.pretty import pretty_script_or_style, pretty_xml_tree, serialize
from calibre.utils.icu import lower as icu_lower
from calibre.utils.icu import numeric_sort_key
from calibre.utils.localization import ngettext
from polyglot.builtins import iteritems, itervalues
from polyglot.functools import lru_cache

class SelectorStatus:
    any_used: bool = False
    any_unused: bool = False

def mark_used_selectors(rules, log, select):
    ans = SelectorStatus()
    for rule in rules:
        for selector in rule.selectorList:
            if getattr(selector, 'calibre_used', False):
                ans.any_used = True
                continue
            try:
                if select.has_matches(selector.selectorText):
                    selector.calibre_used = True
                    ans.any_used = True
                else:
                    ans.any_unused = True
                    selector.calibre_used = False
            except SelectorError:
                # Cannot parse/execute this selector, be safe and assume it
                # matches something
                selector.calibre_used = True
                ans.any_used = True
    return ans

def get_imported_sheets(name, container, sheets, recursion_level=10, sheet=None):
    ans = set()
    sheet = sheet or sheets[name]
    for rule in sheet.cssRules.rulesOfType(CSSRule.IMPORT_RULE):
        if rule.href:
            iname = container.href_to_name(rule.href, name)
            if iname in sheets:
                ans.add(iname)
    if recursion_level > 0:
        for imported_sheet in tuple(ans):
            ans |= get_imported_sheets(imported_sheet, container, sheets, recursion_level=recursion_level-1)
    ans.discard(name)
    return ans

def merge_declarations(first, second):
    for prop in second.getProperties():
        first.setProperty(prop)

def merge_identical_selectors(sheet):
    ' Merge rules that have identical selectors '
    selector_map = defaultdict(list)
    for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
        selector_map[rule.selectorText].append(rule)
    remove = []
    for rule_group in itervalues(selector_map):
        if len(rule_group) > 1:
            for i in range(1, len(rule_group)):
                merge_declarations(rule_group[0].style, rule_group[i].style)
                remove.append(rule_group[i])
    for rule in remove:
        sheet.cssRules.remove(rule)
    return len(remove)

def merge_identical_properties(sheet):
    ' Merge rules having identical properties '
    properties_map = defaultdict(list)

    def declaration_key(declaration):
        return tuple(sorted(
            ((prop.name, prop.propertyValue.value) for prop in declaration.getProperties()),
            key=itemgetter(0)
        ))

    for idx, rule in enumerate(sheet.cssRules):
        if rule.type == CSSRule.STYLE_RULE:
            properties_map[declaration_key(rule.style)].append((idx, rule))

    removals = []
    num_merged = 0
    for rule_group in properties_map.values():
        if len(rule_group) < 2:
            continue
        num_merged += len(rule_group)
        selectors = rule_group[0][1].selectorList
        seen = {s.selectorText for s in selectors}
        rules = iter(rule_group)
        next(rules)
        for idx, rule in rules:
            removals.append(idx)
            for s in rule.selectorList:
                q = s.selectorText
                if q not in seen:
                    seen.add(q)
                    selectors.append(s)
    for idx in sorted(removals, reverse=True):
        sheet.cssRules.pop(idx)
    return num_merged

def remove_unused_selectors_and_rules(rules_container, rules, removal_stats):
    ans = SelectorStatus()
    for r in rules:
        removals = []
        for i, sel in enumerate(r.selectorList):
            if getattr(sel, 'calibre_used', True):
                ans.any_used = True
            else:
                removals.append(i)
        if removals:
            ans.any_unused = True
            if len(removals) == len(r.selectorList):
                rules_container.remove(r)
                removal_stats['rules'] += 1
            else:
                removal_stats['selectors'] += len(removals)
                for i in reversed(removals):
                    del r.selectorList[i]
    return ans

[documentos] def remove_unused_css( container, report=None, remove_unused_classes=False, merge_rules=False, merge_rules_with_identical_properties=False, remove_unreferenced_sheets=False, ): ''' Remove all unused CSS rules from the book. An unused CSS rule is one that does not match any actual content. :param report: An optional callable that takes a single argument. It is called with information about the operations being performed. :param remove_unused_classes: If True, class attributes in the HTML that do not match any CSS rules are also removed. :param merge_rules: If True, rules with identical selectors are merged. :param merge_rules_with_identical_properties: If True, rules with identical properties are merged. :param remove_unreferenced_sheets: If True, stylesheets that are not referenced by any content are removed ''' report = report or (lambda x:x) def safe_parse(name): try: return container.parsed(name) except TypeError: pass sheets = {name:safe_parse(name) for name, mt in iteritems(container.mime_map) if mt in OEB_STYLES} sheets = {k:v for k, v in iteritems(sheets) if v is not None} num_merged = num_rules_merged = 0 if merge_rules: for name, sheet in iteritems(sheets): num = merge_identical_selectors(sheet) if num: container.dirty(name) num_merged += num if merge_rules_with_identical_properties: for name, sheet in iteritems(sheets): num = merge_identical_properties(sheet) if num: container.dirty(name) num_rules_merged += num import_map = {name:get_imported_sheets(name, container, sheets) for name in sheets} unreferenced_sheets = set(sheets) if remove_unused_classes: class_map = {name:{icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} for name, sheet in iteritems(sheets)} style_rules = {name:tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) for name, sheet in iteritems(sheets)} removal_stats = {'rules': 0, 'selectors': 0} num_of_removed_classes = 0 for name, mt in iteritems(container.mime_map): if mt not in OEB_DOCS: continue root = container.parsed(name) select = Select(root, ignore_inappropriate_pseudo_classes=True) used_classes = set() for style in root.xpath('//*[local-name()="style"]'): if style.get('type', 'text/css') == 'text/css' and style.text: sheet = container.parse_css(style.text) if merge_rules: num = merge_identical_selectors(sheet) if num: num_merged += num container.dirty(name) if merge_rules_with_identical_properties: num = merge_identical_properties(sheet) if num: num_rules_merged += num container.dirty(name) if remove_unused_classes: used_classes |= {icu_lower(x) for x in classes_in_rule_list(sheet.cssRules)} imports = get_imported_sheets(name, container, sheets, sheet=sheet) for imported_sheet in imports: unreferenced_sheets.discard(imported_sheet) mark_used_selectors(style_rules[imported_sheet], container.log, select) if remove_unused_classes: used_classes |= class_map[imported_sheet] rules = tuple(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE)) if mark_used_selectors(rules, container.log, select).any_unused: remove_unused_selectors_and_rules(sheet.cssRules, rules, removal_stats) style.text = force_unicode(sheet.cssText, 'utf-8') pretty_script_or_style(container, style) container.dirty(name) for link in root.xpath('//*[local-name()="link" and @href]'): sname = container.href_to_name(link.get('href'), name) if sname not in sheets: continue mark_used_selectors(style_rules[sname], container.log, select) if remove_unused_classes: used_classes |= class_map[sname] unreferenced_sheets.discard(sname) for iname in import_map[sname]: unreferenced_sheets.discard(iname) mark_used_selectors(style_rules[iname], container.log, select) if remove_unused_classes: used_classes |= class_map[iname] if remove_unused_classes: for elem in root.xpath('//*[@class]'): original_classes, classes = elem.get('class', '').split(), [] for x in original_classes: if icu_lower(x) in used_classes: classes.append(x) if len(classes) != len(original_classes): if classes: elem.set('class', ' '.join(classes)) else: del elem.attrib['class'] num_of_removed_classes += len(original_classes) - len(classes) container.dirty(name) for name, sheet in iteritems(sheets): if name in unreferenced_sheets: continue q = remove_unused_selectors_and_rules(sheet.cssRules, style_rules[name], removal_stats) if q.any_unused: container.dirty(name) num_sheets_removed = 0 if remove_unreferenced_sheets and len(unreferenced_sheets): num_sheets_removed += len(unreferenced_sheets) for uname in unreferenced_sheets: container.remove_item(uname) num_changes = num_merged + num_of_removed_classes + num_rules_merged + removal_stats['rules'] + removal_stats['selectors'] + num_sheets_removed if num_changes > 0: if removal_stats['rules']: report(ngettext('Removed one unused CSS style rule', 'Removed {} unused CSS style rules', removal_stats['rules']).format(removal_stats['rules'])) if removal_stats['selectors']: report(ngettext('Removed one unused CSS selector', 'Removed {} unused CSS selectors', removal_stats['selectors']).format(removal_stats['selectors'])) if num_of_removed_classes > 0: report(ngettext('Removed one unused class from the HTML', 'Removed {} unused classes from the HTML', num_of_removed_classes).format(num_of_removed_classes)) if num_merged > 0: report(ngettext('Merged one CSS style rule with identical selectors', 'Merged {} CSS style rules with identical selectors', num_merged).format(num_merged)) if num_rules_merged > 0: report(ngettext('Merged one CSS style rule with identical properties', 'Merged {} CSS style rules with identical properties', num_rules_merged).format(num_rules_merged)) if num_sheets_removed: report(ngettext('Removed one unreferenced stylesheet', 'Removed {} unreferenced stylesheets', num_sheets_removed).format(num_sheets_removed)) if not removal_stats['rules']: report(_('No unused CSS style rules found')) if not removal_stats['selectors']: report(_('No unused CSS selectors found')) if remove_unused_classes and num_of_removed_classes == 0: report(_('No unused class attributes found')) if merge_rules and num_merged == 0: report(_('No style rules that could be merged found')) if remove_unreferenced_sheets and num_sheets_removed == 0: report(_('No unused stylesheets found')) return num_changes > 0

def filter_declaration(style, properties=()):
    changed = False
    for prop in properties:
        if style.removeProperty(prop) != '':
            changed = True
    all_props = set(style.keys())
    for prop in style.getProperties():
        n = normalizers.get(prop.name, None)
        if n is not None:
            normalized = n(prop.name, prop.propertyValue)
            removed = properties.intersection(set(normalized))
            if removed:
                changed = True
                style.removeProperty(prop.name)
                for prop in set(normalized) - removed - all_props:
                    style.setProperty(prop, normalized[prop])
    return changed

def filter_sheet(sheet, properties=()):
    from css_parser.css import CSSRule
    changed = False
    remove = []
    for rule in sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE):
        if filter_declaration(rule.style, properties):
            changed = True
            if rule.style.length == 0:
                remove.append(rule)
    for rule in remove:
        sheet.cssRules.remove(rule)
    return changed

def transform_inline_styles(container, name, transform_sheet, transform_style):
    root = container.parsed(name)
    changed = False
    for style in root.xpath('//*[local-name()="style"]'):
        if style.text and (style.get('type') or 'text/css').lower() == 'text/css':
            sheet = container.parse_css(style.text)
            if transform_sheet(sheet):
                changed = True
                style.text = force_unicode(sheet.cssText, 'utf-8')
                pretty_script_or_style(container, style)
    for elem in root.xpath('//*[@style]'):
        text = elem.get('style', None)
        if text:
            style = container.parse_css(text, is_declaration=True)
            if transform_style(style):
                changed = True
                if style.length == 0:
                    del elem.attrib['style']
                else:
                    elem.set('style', force_unicode(style.getCssText(separator=' '), 'utf-8'))
    return changed

def transform_css(container, transform_sheet=None, transform_style=None, names=()):
    if not names:
        types = OEB_STYLES | OEB_DOCS
        names = []
        for name, mt in iteritems(container.mime_map):
            if mt in types:
                names.append(name)

    doc_changed = False

    for name in names:
        mt = container.mime_map[name]
        if mt in OEB_STYLES:
            sheet = container.parsed(name)
            if transform_sheet(sheet):
                container.dirty(name)
                doc_changed = True
        elif mt in OEB_DOCS:
            if transform_inline_styles(container, name, transform_sheet, transform_style):
                container.dirty(name)
                doc_changed = True

    return doc_changed

[documentos] def filter_css(container, properties, names=()): ''' Remove the specified CSS properties from all CSS rules in the book. :param properties: Set of properties to remove. For example: :code:`{'font-family', 'color'}`. :param names: The files from which to remove the properties. Defaults to all HTML and CSS files in the book. ''' properties = normalize_filter_css(properties) return transform_css(container, transform_sheet=partial(filter_sheet, properties=properties), transform_style=partial(filter_declaration, properties=properties), names=names)

def _classes_in_selector(selector, classes):
    for attr in ('selector', 'subselector', 'parsed_tree'):
        s = getattr(selector, attr, None)
        if s is not None:
            _classes_in_selector(s, classes)
    cn = getattr(selector, 'class_name', None)
    if cn is not None:
        classes.add(cn)

@lru_cache(maxsize=4096)
def classes_in_selector(text):
    classes = set()
    try:
        for selector in parse(text):
            _classes_in_selector(selector, classes)
    except SelectorSyntaxError:
        pass
    return classes

def classes_in_rule_list(css_rules):
    classes = set()
    for rule in css_rules:
        if rule.type == rule.STYLE_RULE:
            classes |= classes_in_selector(rule.selectorText)
        elif hasattr(rule, 'cssRules'):
            classes |= classes_in_rule_list(rule.cssRules)
    return classes

def iter_declarations(sheet_or_rule):
    if hasattr(sheet_or_rule, 'cssRules'):
        for rule in sheet_or_rule.cssRules:
            yield from iter_declarations(rule)
    elif hasattr(sheet_or_rule, 'style'):
        yield sheet_or_rule.style
    elif isinstance(sheet_or_rule, CSSStyleDeclaration):
        yield sheet_or_rule

def remove_property_value(prop, predicate):
    ''' Remove the Values that match the predicate from this property. If all
    values of the property would be removed, the property is removed from its
    parent instead. Note that this means the property must have a parent (a
    CSSStyleDeclaration). '''
    removed_vals = list(filter(predicate, prop.propertyValue))
    if len(removed_vals) == len(prop.propertyValue):
        prop.parent.removeProperty(prop.name)
    else:
        x = css_text(prop.propertyValue)
        for v in removed_vals:
            x = x.replace(css_text(v), '').strip()
        prop.propertyValue.cssText = x
    return bool(removed_vals)

RULE_PRIORITIES = {t:i for i, t in enumerate((CSSRule.COMMENT, CSSRule.CHARSET_RULE, CSSRule.IMPORT_RULE, CSSRule.NAMESPACE_RULE))}

def sort_sheet(container, sheet_or_text):
    ''' Sort the rules in a stylesheet. Note that in the general case this can
    change the effective styles, but for most common sheets, it should be safe.
    '''
    sheet = container.parse_css(sheet_or_text) if isinstance(sheet_or_text, str) else sheet_or_text

    def text_sort_key(x):
        return numeric_sort_key(str(x or ''))

    def selector_sort_key(x):
        return (x.specificity, text_sort_key(x.selectorText))

    def rule_sort_key(rule):
        primary = RULE_PRIORITIES.get(rule.type, len(RULE_PRIORITIES))
        secondary = text_sort_key(getattr(rule, 'atkeyword', '') or '')
        tertiary = None
        if rule.type == CSSRule.STYLE_RULE:
            primary += 1
            selectors = sorted(rule.selectorList, key=selector_sort_key)
            tertiary = selector_sort_key(selectors[0])
            rule.selectorText = ', '.join(s.selectorText for s in selectors)
        elif rule.type == CSSRule.FONT_FACE_RULE:
            try:
                tertiary = text_sort_key(rule.style.getPropertyValue('font-family'))
            except Exception:
                pass

        return primary, secondary, tertiary
    sheet.cssRules.sort(key=rule_sort_key)
    return sheet

def add_stylesheet_links(container, name, text):
    root = container.parse_xhtml(text, name)
    head = root.xpath('//*[local-name() = "head"]')
    if not head:
        return
    head = head[0]
    sheets = tuple(container.manifest_items_of_type(lambda mt: mt in OEB_STYLES))
    if not sheets:
        return
    for sname in sheets:
        link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(sname, name))
        head.append(link)
    pretty_xml_tree(head)
    return serialize(root, 'text/html')

def rename_class_in_rule_list(css_rules, old_name, new_name):
    # this regex will not match class names inside attribute value selectors
    # and it will match id selectors that contain .old_name but its the best
    # that can be done without implementing a full parser for CSS selectors
    pat = re.compile(rf'(?<=\.){re.escape(old_name)}(?:\W|$)')

    def repl(m):
        return m.group().replace(old_name, new_name)

    changed = False
    for rule in css_rules:
        if rule.type == rule.STYLE_RULE:
            old = rule.selectorText
            q = pat.sub(repl, old)
            if q != old:
                changed = True
                rule.selectorText = q
        elif hasattr(rule, 'cssRules'):
            if rename_class_in_rule_list(rule.cssRules, old_name, new_name):
                changed = True
    return changed

def rename_class_in_doc(container, root, old_name, new_name):
    changed = False
    pat = re.compile(rf'(?:^|\W){re.escape(old_name)}(?:\W|$)')

    def repl(m):
        return m.group().replace(old_name, new_name)

    for elem in root.xpath('//*[@class]'):
        old = elem.get('class')
        if old:
            new = pat.sub(repl, old)
            if new != old:
                changed = True
                elem.set('class', new)
    for style in root.xpath('//*[local-name()="style"]'):
        if style.get('type', 'text/css') == 'text/css' and style.text:
            sheet = container.parse_css(style.text)
            if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
                changed = True
                style.text = force_unicode(sheet.cssText, 'utf-8')
    return changed

def rename_class(container, old_name, new_name):
    changed = False
    if not old_name or old_name == new_name:
        return changed
    for sheet_name in container.manifest_items_of_type(lambda mt: mt in OEB_STYLES):
        sheet = container.parsed(sheet_name)
        if rename_class_in_rule_list(sheet.cssRules, old_name, new_name):
            container.dirty(sheet_name)
            changed = True
    for doc_name in container.manifest_items_of_type(lambda mt: mt in OEB_DOCS):
        doc = container.parsed(doc_name)
        if rename_class_in_doc(container, doc, old_name, new_name):
            container.dirty(doc_name)
            changed = True
    return changed

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.fonts

Código fuente para calibre.ebooks.oeb.polish.fonts

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

from tinycss.fonts3 import parse_font, parse_font_family, serialize_font, serialize_font_family

from calibre.ebooks.oeb.base import css_text
from calibre.ebooks.oeb.normalize_css import normalize_font
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES
from polyglot.builtins import iteritems

def unquote(x):
    if x and len(x) > 1 and x[0] == x[-1] and x[0] in ('"', "'"):
        x = x[1:-1]
    return x

def font_family_data_from_declaration(style, families):
    font_families = []
    f = style.getProperty('font')
    if f is not None:
        f = normalize_font(f.propertyValue, font_family_as_list=True).get('font-family', None)
        if f is not None:
            font_families = [unquote(x) for x in f]
    f = style.getProperty('font-family')
    if f is not None:
        font_families = parse_font_family(css_text(f.propertyValue))

    for f in font_families:
        families[f] = families.get(f, False)

def font_family_data_from_sheet(sheet, families):
    for rule in sheet.cssRules:
        if rule.type == rule.STYLE_RULE:
            font_family_data_from_declaration(rule.style, families)
        elif rule.type == rule.FONT_FACE_RULE:
            ff = rule.style.getProperty('font-family')
            if ff is not None:
                for f in parse_font_family(css_text(ff.propertyValue)):
                    families[f] = True

def font_family_data(container):
    families = {}
    for name, mt in iteritems(container.mime_map):
        if mt in OEB_STYLES:
            sheet = container.parsed(name)
            font_family_data_from_sheet(sheet, families)
        elif mt in OEB_DOCS:
            root = container.parsed(name)
            for style in root.xpath('//*[local-name() = "style"]'):
                if style.text and style.get('type', 'text/css').lower() == 'text/css':
                    sheet = container.parse_css(style.text)
                    font_family_data_from_sheet(sheet, families)
            for style in root.xpath('//*/@style'):
                if style:
                    style = container.parse_css(style, is_declaration=True)
                    font_family_data_from_declaration(style, families)
    return families

def change_font_in_declaration(style, old_name, new_name=None):
    changed = False
    ff = style.getProperty('font-family')
    if ff is not None:
        fams = parse_font_family(css_text(ff.propertyValue))
        nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
        if fams != nfams:
            if nfams:
                ff.propertyValue.cssText = serialize_font_family(nfams)
            else:
                style.removeProperty(ff.name)
            changed = True
    ff = style.getProperty('font')
    if ff is not None:
        props = parse_font(css_text(ff.propertyValue))
        fams = props.get('font-family') or []
        nfams = list(filter(None, [new_name if x == old_name else x for x in fams]))
        if fams != nfams:
            props['font-family'] = nfams
            if nfams:
                ff.propertyValue.cssText = serialize_font(props)
            else:
                style.removeProperty(ff.name)
            changed = True
    return changed

def remove_embedded_font(container, sheet, rule, sheet_name):
    src = getattr(rule.style.getProperty('src'), 'value', None)
    if src is not None:
        if src.startswith('url('):
            src = src[4:-1]
    sheet.cssRules.remove(rule)
    if src:
        src = unquote(src)
        name = container.href_to_name(src, sheet_name)
        if container.has_name(name):
            container.remove_item(name)

def change_font_in_sheet(container, sheet, old_name, new_name, sheet_name):
    changed = False
    removals = []
    for rule in sheet.cssRules:
        if rule.type == rule.STYLE_RULE:
            changed |= change_font_in_declaration(rule.style, old_name, new_name)
        elif rule.type == rule.FONT_FACE_RULE:
            ff = rule.style.getProperty('font-family')
            if ff is not None:
                families = {x for x in parse_font_family(css_text(ff.propertyValue))}
                if old_name in families:
                    changed = True
                    removals.append(rule)
    for rule in reversed(removals):
        remove_embedded_font(container, sheet, rule, sheet_name)
    return changed

[documentos] def change_font(container, old_name, new_name=None): ''' Change a font family from old_name to new_name. Changes all occurrences of the font family in stylesheets, style tags and style attributes. If the old_name refers to an embedded font, it is removed. You can set new_name to None to remove the font family instead of changing it. ''' changed = False for name, mt in tuple(iteritems(container.mime_map)): if mt in OEB_STYLES: sheet = container.parsed(name) if change_font_in_sheet(container, sheet, old_name, new_name, name): container.dirty(name) changed = True elif mt in OEB_DOCS: root = container.parsed(name) for style in root.xpath('//*[local-name() = "style"]'): if style.text and style.get('type', 'text/css').lower() == 'text/css': sheet = container.parse_css(style.text) if change_font_in_sheet(container, sheet, old_name, new_name, name): container.dirty(name) changed = True for elem in root.xpath('//*[@style]'): style = elem.get('style', '') if style: style = container.parse_css(style, is_declaration=True) if change_font_in_declaration(style, old_name, new_name): style = css_text(style).strip().rstrip(';').strip() if style: elem.set('style', style) else: del elem.attrib['style'] container.dirty(name) changed = True return changed

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.jacket

Código fuente para calibre.ebooks.oeb.polish.jacket

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from calibre.customize.ui import output_profiles
from calibre.ebooks.conversion.config import load_defaults
from calibre.ebooks.oeb.base import OPF, XPath
from calibre.ebooks.oeb.polish.cover import find_cover_page
from calibre.ebooks.oeb.transforms.jacket import referenced_images
from calibre.ebooks.oeb.transforms.jacket import render_jacket as render

def render_jacket(container, jacket):
    mi = container.mi
    ps = load_defaults('page_setup')
    op = ps.get('output_profile', 'default')
    opmap = {x.short_name:x for x in output_profiles()}
    output_profile = opmap.get(op, opmap['default'])
    root = render(mi, output_profile)
    for img, path in referenced_images(root):
        container.log('Embedding referenced image: %s into jacket' % path)
        ext = path.rpartition('.')[-1]
        jacket_item = container.generate_item('jacket_image.'+ext, id_prefix='jacket_img')
        name = container.href_to_name(jacket_item.get('href'), container.opf_name)
        with open(path, 'rb') as f:
            container.parsed_cache[name] = f.read()
            container.commit_item(name)
        href = container.name_to_href(name, jacket)
        img.set('src', href)
    return root

def is_legacy_jacket(root):
    return len(root.xpath(
        '//*[starts-with(@class,"calibrerescale") and (local-name()="h1" or local-name()="h2")]')) > 0

def is_current_jacket(root):
    return len(XPath(
        '//h:meta[@name="calibre-content" and @content="jacket"]')(root)) > 0

def find_existing_jacket(container):
    for item in container.spine_items:
        name = container.abspath_to_name(item)
        if container.book_type == 'azw3':
            root = container.parsed(name)
            if is_current_jacket(root):
                return name
        else:
            if name.rpartition('/')[-1].startswith('jacket') and name.endswith('.xhtml'):
                root = container.parsed(name)
                if is_current_jacket(root) or is_legacy_jacket(root):
                    return name

def replace_jacket(container, name):
    root = render_jacket(container, name)
    container.parsed_cache[name] = root
    container.dirty(name)

[documentos] def remove_jacket(container): ' Remove an existing jacket, if any. Returns False if no existing jacket was found. ' name = find_existing_jacket(container) if name is not None: remove_jacket_images(container, name) container.remove_item(name) return True return False

def remove_jacket_images(container, name):
    root = container.parsed_cache[name]
    for img in root.xpath('//*[local-name() = "img" and @src]'):
        iname = container.href_to_name(img.get('src'), name)
        if container.has_name(iname):
            container.remove_item(iname)

[documentos] def add_or_replace_jacket(container): ''' Either create a new jacket from the book's metadata or replace an existing jacket. Returns True if an existing jacket was replaced. ''' name = find_existing_jacket(container) found = True if name is None: jacket_item = container.generate_item('jacket.xhtml', id_prefix='jacket') name = container.href_to_name(jacket_item.get('href'), container.opf_name) found = False if found: remove_jacket_images(container, name) replace_jacket(container, name) if not found: # Insert new jacket into spine index = 0 sp = container.abspath_to_name(next(container.spine_items)) if sp == find_cover_page(container): index = 1 itemref = container.opf.makeelement(OPF('itemref'), idref=jacket_item.get('id')) container.insert_into_xml(container.opf_xpath('//opf:spine')[0], itemref, index=index) return found

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.pretty

Código fuente para calibre.ebooks.oeb.polish.pretty

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import textwrap

# from lxml.etree import Element
from calibre import force_unicode
from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, SVG, XHTML, XPNSMAP, barename, serialize
from calibre.ebooks.oeb.polish.container import OPF_NAMESPACES
from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.utils.icu import sort_key
from polyglot.builtins import iteritems

def isspace(x):
    return not x.strip('\u0009\u000a\u000c\u000d\u0020')

def pretty_xml_tree(elem, level=0, indent='  '):
    ''' XML beautifier, assumes that elements that have children do not have
    textual content.  Also assumes that there is no text immediately after
    closing tags. These are true for opf/ncx and container.xml files. If either
    of the assumptions are violated, there should be no data loss, but pretty
    printing won't produce optimal results.'''
    if (not elem.text and len(elem) > 0) or (elem.text and isspace(elem.text)):
        elem.text = '\n' + (indent * (level+1))
    for i, child in enumerate(elem):
        pretty_xml_tree(child, level=level+1, indent=indent)
        if not child.tail or isspace(child.tail):
            l = level + 1
            if i == len(elem) - 1:
                l -= 1
            child.tail = '\n' + (indent * l)

def pretty_opf(root):
    # Put all dc: tags first starting with title and author. Preserve order for
    # the rest.
    def dckey(x):
        return {'title':0, 'creator':1}.get(barename(x.tag), 2)
    for metadata in root.xpath('//opf:metadata', namespaces=OPF_NAMESPACES):
        dc_tags = metadata.xpath('./*[namespace-uri()="%s"]' % OPF_NAMESPACES['dc'])
        dc_tags.sort(key=dckey)
        for x in reversed(dc_tags):
            metadata.insert(0, x)

    # Group items in the manifest
    spine_ids = root.xpath('//opf:spine/opf:itemref/@idref', namespaces=OPF_NAMESPACES)
    spine_ids = {x:i for i, x in enumerate(spine_ids)}

    def manifest_key(x):
        mt = x.get('media-type', '')
        href = x.get('href', '')
        ext = href.rpartition('.')[-1].lower()
        cat = 1000
        if mt in OEB_DOCS:
            cat = 0
        elif mt == guess_type('a.ncx'):
            cat = 1
        elif mt in OEB_STYLES:
            cat = 2
        elif mt.startswith('image/'):
            cat = 3
        elif ext in {'otf', 'ttf', 'woff', 'woff2'}:
            cat = 4
        elif mt.startswith('audio/'):
            cat = 5
        elif mt.startswith('video/'):
            cat = 6

        if cat == 0:
            i = spine_ids.get(x.get('id', None), 1000000000)
        else:
            i = sort_key(href)
        return (cat, i)

    for manifest in root.xpath('//opf:manifest', namespaces=OPF_NAMESPACES):
        try:
            children = sorted(manifest, key=manifest_key)
        except AttributeError:
            continue  # There are comments so dont sort since that would mess up the comments
        for x in reversed(children):
            manifest.insert(0, x)

SVG_TAG = SVG('svg')
BLOCK_TAGS = frozenset(map(XHTML, (
    'address', 'article', 'aside', 'audio', 'blockquote', 'body', 'canvas', 'col', 'colgroup', 'dd',
    'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
    'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li',
    'noscript', 'ol', 'output', 'p', 'pre', 'script', 'section', 'style', 'table', 'tbody', 'td',
    'tfoot', 'th', 'thead', 'tr', 'ul', 'video', 'img'))) | {SVG_TAG}

def isblock(x):
    if callable(x.tag) or not x.tag:
        return True
    if x.tag in BLOCK_TAGS:
        return True
    return False

def has_only_blocks(x):
    if hasattr(x.tag, 'split') and len(x) == 0:
        # Tag with no children,
        return False
    if x.text and not isspace(x.text):
        return False
    for child in x:
        if not isblock(child) or (child.tail and not isspace(child.tail)):
            return False
    return True

def indent_for_tag(x):
    prev = x.getprevious()
    x = x.getparent().text if prev is None else prev.tail
    if not x:
        return ''
    s = x.rpartition('\n')[-1]
    return s if isspace(s) else ''

def set_indent(elem, attr, indent):
    x = getattr(elem, attr)
    if not x:
        x = indent
    else:
        lines = x.splitlines()
        if isspace(lines[-1]):
            lines[-1] = indent
        else:
            lines.append(indent)
        x = '\n'.join(lines)
    setattr(elem, attr, x)

def pretty_block(parent, level=1, indent='  '):
    ''' Surround block tags with blank lines and recurse into child block tags
    that contain only other block tags '''
    if not parent.text or isspace(parent.text):
        parent.text = ''
    nn = '\n' if hasattr(parent.tag, 'strip') and barename(parent.tag) in {'tr', 'td', 'th'} else '\n\n'
    parent.text = parent.text + nn + (indent * level)
    for i, child in enumerate(parent):
        if isblock(child) and has_only_blocks(child):
            pretty_block(child, level=level+1, indent=indent)
        elif child.tag == SVG_TAG:
            pretty_xml_tree(child, level=level, indent=indent)
        l = level
        if i == len(parent) - 1:
            l -= 1
        if not child.tail or isspace(child.tail):
            child.tail = ''
        child.tail = child.tail + nn + (indent * l)

def pretty_script_or_style(container, child):
    if child.text:
        indent = indent_for_tag(child)
        if child.tag.endswith('style'):
            child.text = force_unicode(pretty_css(container, '', child.text), 'utf-8')
        child.text = textwrap.dedent(child.text)
        child.text = '\n' + '\n'.join([(indent + x) if x else '' for x in child.text.splitlines()])
        set_indent(child, 'text', indent)

def pretty_html_tree(container, root):
    root.text = '\n\n'
    for child in root:
        child.tail = '\n\n'
        if hasattr(child.tag, 'endswith') and child.tag.endswith('}head'):
            pretty_xml_tree(child)
    for body in root.findall('h:body', namespaces=XPNSMAP):
        pretty_block(body)
        # Special case the handling of a body that contains a single block tag
        # with all content. In this case we prettify the containing block tag
        # even if it has non block children.
        if (len(body) == 1 and not callable(body[0].tag) and isblock(body[0]) and not has_only_blocks(
            body[0]) and barename(body[0].tag) not in (
                    'pre', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6') and len(body[0]) > 0):
            pretty_block(body[0], level=2)

    if container is not None:
        # Handle <script> and <style> tags
        for child in root.xpath('//*[local-name()="script" or local-name()="style"]'):
            pretty_script_or_style(container, child)

[documentos] def fix_html(container, raw): ' Fix any parsing errors in the HTML represented as a string in raw. Fixing is done using the HTML5 parsing algorithm. ' root = container.parse_xhtml(raw) return serialize(root, 'text/html')

[documentos] def pretty_html(container, name, raw): ' Pretty print the HTML represented as a string in raw ' root = container.parse_xhtml(raw) pretty_html_tree(container, root) return serialize(root, 'text/html')

[documentos] def pretty_css(container, name, raw): ' Pretty print the CSS represented as a string in raw ' sheet = container.parse_css(raw) return serialize(sheet, 'text/css')

[documentos] def pretty_xml(container, name, raw): ' Pretty print the XML represented as a string in raw. If ``name`` is the name of the OPF, extra OPF-specific prettying is performed. ' root = container.parse_xml(raw) if name == container.opf_name: pretty_opf(root) pretty_xml_tree(root) return serialize(root, 'text/xml')

[documentos] def fix_all_html(container): ' Fix any parsing errors in all HTML files in the container. Fixing is done using the HTML5 parsing algorithm. ' for name, mt in iteritems(container.mime_map): if mt in OEB_DOCS: container.parsed(name) container.dirty(name)

[documentos] def pretty_all(container): ' Pretty print all HTML/CSS/XML files in the container ' xml_types = {guess_type('a.ncx'), guess_type('a.xml'), guess_type('a.svg')} for name, mt in iteritems(container.mime_map): prettied = False if mt in OEB_DOCS: pretty_html_tree(container, container.parsed(name)) prettied = True elif mt in OEB_STYLES: container.parsed(name) prettied = True elif name == container.opf_name: root = container.parsed(name) pretty_opf(root) pretty_xml_tree(root) prettied = True elif mt in xml_types: pretty_xml_tree(container.parsed(name)) prettied = True if prettied: container.dirty(name)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.replace

Código fuente para calibre.ebooks.oeb.polish.replace

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import codecs
import os
import posixpath
import shutil
from collections import Counter, defaultdict
from functools import partial

from calibre import sanitize_file_name
from calibre.ebooks.chardet import strip_encoding_declarations
from calibre.ebooks.oeb.base import css_text
from calibre.ebooks.oeb.polish.css import iter_declarations, remove_property_value
from calibre.ebooks.oeb.polish.utils import extract
from polyglot.builtins import iteritems, itervalues
from polyglot.urllib import urlparse, urlunparse

class LinkReplacer:

    def __init__(self, base, container, link_map, frag_map):
        self.base = base
        self.frag_map = frag_map
        self.link_map = link_map
        self.container = container
        self.replaced = False

    def __call__(self, url):
        if url and url.startswith('#'):
            repl = self.frag_map(self.base, url[1:])
            if not repl or repl == url[1:]:
                return url
            self.replaced = True
            return '#' + repl
        name = self.container.href_to_name(url, self.base)
        if not name:
            return url
        nname = self.link_map.get(name, None)
        if not nname:
            return url
        purl = urlparse(url)
        href = self.container.name_to_href(nname, self.base)
        if purl.fragment:
            nfrag = self.frag_map(name, purl.fragment)
            if nfrag:
                href += '#%s'%nfrag
        if href != url:
            self.replaced = True
        return href

class IdReplacer:

    def __init__(self, base, container, id_map):
        self.base, self.container, self.replaced = base, container, False
        self.id_map = id_map

    def __call__(self, url):
        if url and url.startswith('#'):
            repl = self.id_map.get(self.base, {}).get(url[1:])
            if repl is None or repl == url[1:]:
                return url
            self.replaced = True
            return '#' + repl
        name = self.container.href_to_name(url, self.base)
        if not name:
            return url
        id_map = self.id_map.get(name)
        if id_map is None:
            return url
        purl = urlparse(url)
        nfrag = id_map.get(purl.fragment)
        if nfrag is None:
            return url
        purl = purl._replace(fragment=nfrag)
        href = urlunparse(purl)
        if href != url:
            self.replaced = True
        return href

class LinkRebaser:

    def __init__(self, container, old_name, new_name):
        self.old_name, self.new_name = old_name, new_name
        self.container = container
        self.replaced = False

    def __call__(self, url):
        if url and url.startswith('#'):
            return url
        purl = urlparse(url)
        frag = purl.fragment
        name = self.container.href_to_name(url, self.old_name)
        if not name:
            return url
        if name == self.old_name:
            name = self.new_name
        href = self.container.name_to_href(name, self.new_name)
        if frag:
            href += '#' + frag
        if href != url:
            self.replaced = True
        return href

[documentos] def replace_links(container, link_map, frag_map=lambda name, frag:frag, replace_in_opf=False): ''' Replace links to files in the container. Will iterate over all files in the container and change the specified links in them. :param link_map: A mapping of old canonical name to new canonical name. For example: :code:`{'images/old.png': 'images/new.png'}` :param frag_map: A callable that takes two arguments ``(name, anchor)`` and returns a new anchor. This is useful if you need to change the anchors in HTML files. By default, it does nothing. :param replace_in_opf: If False, links are not replaced in the OPF file. ''' for name, media_type in iteritems(container.mime_map): if name == container.opf_name and not replace_in_opf: continue repl = LinkReplacer(name, container, link_map, frag_map) container.replace_links(name, repl)

def replace_ids(container, id_map):
    '''
    Replace all links in the container that pointed to the changed ids.

    :param id_map: A mapping of {name:id_map} where each id_map is a mapping of {old_id:new_id}
    :return: True iff at least one link was changed

    '''
    changed = False
    for name, media_type in iteritems(container.mime_map):
        repl = IdReplacer(name, container, id_map)
        container.replace_links(name, repl)
        if name == container.opf_name:
            imap = id_map.get(name, {})
            for item in container.opf_xpath('//*[@idref]'):
                old_id = item.get('idref')
                if old_id is not None:
                    new_id = imap.get(old_id)
                    if new_id is not None:
                        item.set('idref', new_id)
        if repl.replaced:
            changed = True
    return changed

def smarten_punctuation(container, report):
    from calibre.ebooks.conversion.preprocess import smarten_punctuation
    smartened = False
    for path in container.spine_items:
        name = container.abspath_to_name(path)
        changed = False
        with container.open(name, 'r+b') as f:
            html = container.decode(f.read())
            newhtml = smarten_punctuation(html, container.log)
            if newhtml != html:
                changed = True
                report(_('Smartened punctuation in: %s')%name)
                newhtml = strip_encoding_declarations(newhtml)
                f.seek(0)
                f.truncate()
                f.write(codecs.BOM_UTF8 + newhtml.encode('utf-8'))
        if changed:
            # Add an encoding declaration (it will be added automatically when
            # serialized)
            root = container.parsed(name)
            for m in root.xpath('descendant::*[local-name()="meta" and @http-equiv]'):
                m.getparent().remove(m)
            container.dirty(name)
            smartened = True
    if not smartened:
        report(_('No punctuation that could be smartened found'))
    return smartened

[documentos] def rename_files(container, file_map): ''' Rename files in the container, automatically updating all links to them. :param file_map: A mapping of old canonical name to new canonical name, for example: :code:`{'text/chapter1.html': 'chapter1.html'}`. ''' overlap = set(file_map).intersection(set(itervalues(file_map))) if overlap: raise ValueError('Circular rename detected. The files %s are both rename targets and destinations' % ', '.join(overlap)) for name, dest in iteritems(file_map): if container.exists(dest): if name != dest and name.lower() == dest.lower(): # A case change on an OS with a case insensitive file-system. continue raise ValueError('Cannot rename {0} to {1} as {1} already exists'.format(name, dest)) if len(tuple(itervalues(file_map))) != len(set(itervalues(file_map))): raise ValueError('Cannot rename, the set of destination files contains duplicates') link_map = {} for current_name, new_name in iteritems(file_map): container.rename(current_name, new_name) if new_name != container.opf_name: # OPF is handled by the container link_map[current_name] = new_name replace_links(container, link_map, replace_in_opf=True)

def replace_file(container, name, path, basename, force_mt=None):
    dirname, base = name.rpartition('/')[0::2]
    nname = sanitize_file_name(basename)
    if dirname:
        nname = dirname + '/' + nname
    with open(path, 'rb') as src:
        if name != nname:
            count = 0
            b, e = nname.rpartition('.')[0::2]
            while container.exists(nname):
                count += 1
                nname = b + ('_%d.%s' % (count, e))
            rename_files(container, {name:nname})
            mt = force_mt or container.guess_type(nname)
            container.mime_map[nname] = mt
            for itemid, q in iteritems(container.manifest_id_map):
                if q == nname:
                    for item in container.opf_xpath('//opf:manifest/opf:item[@href and @id="%s"]' % itemid):
                        item.set('media-type', mt)
        container.dirty(container.opf_name)
        with container.open(nname, 'wb') as dest:
            shutil.copyfileobj(src, dest)

def mt_to_category(container, mt):
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
    from calibre.ebooks.oeb.polish.utils import OEB_FONTS, guess_type
    if mt in OEB_DOCS:
        category = 'text'
    elif mt in OEB_STYLES:
        category = 'style'
    elif mt in OEB_FONTS:
        category = 'font'
    elif mt == guess_type('a.opf'):
        category = 'opf'
    elif mt == guess_type('a.ncx'):
        category = 'toc'
    else:
        category = mt.partition('/')[0]
    return category

[documentos] def get_recommended_folders(container, names): ''' Return the folders that are recommended for the given filenames. The recommendation is based on where the majority of files of the same type are located in the container. If no files of a particular type are present, the recommended folder is assumed to be the folder containing the OPF file. ''' from calibre.ebooks.oeb.polish.utils import guess_type counts = defaultdict(Counter) for name, mt in iteritems(container.mime_map): folder = name.rpartition('/')[0] if '/' in name else '' counts[mt_to_category(container, mt)][folder] += 1 try: opf_folder = counts['opf'].most_common(1)[0][0] except KeyError: opf_folder = '' recommendations = {category:counter.most_common(1)[0][0] for category, counter in iteritems(counts)} return {n:recommendations.get(mt_to_category(container, guess_type(os.path.basename(n))), opf_folder) for n in names}

def normalize_case(container, val):

    def safe_listdir(x):
        try:
            return os.listdir(x)
        except OSError:
            return ()

    parts = val.split('/')
    ans = []
    for i in range(len(parts)):
        q = '/'.join(parts[:i+1])
        x = container.name_to_abspath(q)
        xl = parts[i].lower()
        candidates = [c for c in safe_listdir(os.path.dirname(x)) if c != parts[i] and c.lower() == xl]
        ans.append(candidates[0] if candidates else parts[i])
    return '/'.join(ans)

def rationalize_folders(container, folder_type_map):
    all_names = set(container.mime_map)
    new_names = set()
    name_map = {}
    for key in tuple(folder_type_map):
        val = folder_type_map[key]
        folder_type_map[key] = normalize_case(container, val)
    for name in all_names:
        if name.startswith('META-INF/'):
            continue
        category = mt_to_category(container, container.mime_map[name])
        folder = folder_type_map.get(category, None)
        if folder is not None:
            bn = posixpath.basename(name)
            new_name = posixpath.join(folder, bn)
            if new_name != name:
                c = 0
                while new_name in all_names or new_name in new_names:
                    c += 1
                    n, ext = bn.rpartition('.')[0::2]
                    new_name = posixpath.join(folder, '%s_%d.%s' % (n, c, ext))
                name_map[name] = new_name
                new_names.add(new_name)
    return name_map

def remove_links_in_sheet(href_to_name, sheet, predicate):
    import_rules_to_remove = []
    changed = False
    for i, r in enumerate(sheet):
        if r.type == r.IMPORT_RULE:
            name = href_to_name(r.href)
            if predicate(name, r.href, None):
                import_rules_to_remove.append(i)
    for i in sorted(import_rules_to_remove, reverse=True):
        sheet.deleteRule(i)
        changed = True

    for dec in iter_declarations(sheet):
        changed = remove_links_in_declaration(href_to_name, dec, predicate) or changed
    return changed

def remove_links_in_declaration(href_to_name, style, predicate):
    def check_pval(v):
        if v.type == v.URI:
            name = href_to_name(v.uri)
            return predicate(name, v.uri, None)
        return False

    changed = False

    for p in tuple(style.getProperties(all=True)):
        changed = remove_property_value(p, check_pval) or changed
    return changed

def remove_links_to(container, predicate):
    ''' predicate must be a function that takes the arguments (name, href,
    fragment=None) and returns True iff the link should be removed '''
    from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES, XHTML, XPath, iterlinks
    stylepath = XPath('//h:style')
    styleattrpath = XPath('//*[@style]')
    changed = set()
    for name, mt in iteritems(container.mime_map):
        removed = False
        if mt in OEB_DOCS:
            root = container.parsed(name)
            for el, attr, href, pos in iterlinks(root, find_links_in_css=False):
                hname = container.href_to_name(href, name)
                frag = href.partition('#')[-1]
                if predicate(hname, href, frag):
                    if attr is None:
                        el.text = None
                    else:
                        if el.tag == XHTML('link') or el.tag == XHTML('img'):
                            extract(el)
                        else:
                            del el.attrib[attr]
                    removed = True
            for tag in stylepath(root):
                if tag.text and (tag.get('type') or 'text/css').lower() == 'text/css':
                    sheet = container.parse_css(tag.text)
                    if remove_links_in_sheet(partial(container.href_to_name, base=name), sheet, predicate):
                        tag.text = css_text(sheet)
                        removed = True
            for tag in styleattrpath(root):
                style = tag.get('style')
                if style:
                    style = container.parse_css(style, is_declaration=True)
                    if remove_links_in_declaration(partial(container.href_to_name, base=name), style, predicate):
                        removed = True
                        tag.set('style', css_text(style))
        elif mt in OEB_STYLES:
            removed = remove_links_in_sheet(partial(container.href_to_name, base=name), container.parsed(name), predicate)
        if removed:
            changed.add(name)
    for i in changed:
        container.dirty(i)
    return changed

def get_spine_order_for_all_files(container):
    linear_names, non_linear_names = [], []
    for name, is_linear in container.spine_names:
        (linear_names if is_linear else non_linear_names).append(name)
    all_names = linear_names + non_linear_names
    spine_names = frozenset(all_names)
    ans = {}
    for spine_pos, name in enumerate(all_names):
        ans.setdefault(name, (spine_pos, -1))
        for i, href in enumerate(container.iterlinks(name, get_line_numbers=False)):
            lname = container.href_to_name(href, name)
            if lname not in spine_names:
                ans.setdefault(lname, (spine_pos, i))
    return ans

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.split

Código fuente para calibre.ebooks.oeb.polish.split

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'

import copy
import os
import re

from calibre.ebooks.oeb.base import OEB_DOCS, OPF, XHTML, XPNSMAP, XPath, barename
from calibre.ebooks.oeb.polish.errors import MalformedMarkup
from calibre.ebooks.oeb.polish.replace import LinkRebaser
from calibre.ebooks.oeb.polish.toc import node_from_loc
from polyglot.builtins import iteritems, string_or_bytes
from polyglot.urllib import urlparse

class AbortError(ValueError):
    pass

def in_table(node):
    while node is not None:
        if node.tag.endswith('}table'):
            return True
        node = node.getparent()
    return False

def adjust_split_point(split_point, log):
    '''
    Move the split point up its ancestor chain if it has no content
    before it. This handles the common case:
    <div id="chapter1"><h2>Chapter 1</h2>...</div> with a page break on the
    h2.
    '''
    sp = split_point
    while True:
        parent = sp.getparent()
        if (
            parent is None or
            barename(parent.tag) in {'body', 'html'} or
            (parent.text and parent.text.strip()) or
            parent.index(sp) > 0
        ):
            break
        sp = parent

    if sp is not split_point:
        log.debug('Adjusted split point to ancestor')

    return sp

def get_body(root):
    return root.find('h:body', namespaces=XPNSMAP)

def do_split(split_point, log, before=True):
    '''
    Split tree into a *before* and an *after* tree at ``split_point``.

    :param split_point: The Element at which to split
    :param before: If True tree is split before split_point, otherwise after split_point
    :return: before_tree, after_tree
    '''
    if before:
        # We cannot adjust for after since moving an after split point to a
        # parent will cause breakage if the parent contains any content
        # after the original split point
        split_point = adjust_split_point(split_point, log)
    tree         = split_point.getroottree()
    path         = tree.getpath(split_point)

    tree, tree2  = copy.deepcopy(tree), copy.deepcopy(tree)
    root, root2  = tree.getroot(), tree2.getroot()
    body, body2  = map(get_body, (root, root2))
    split_point  = root.xpath(path)[0]
    split_point2 = root2.xpath(path)[0]

    def nix_element(elem, top=True):
        # Remove elem unless top is False in which case replace elem by its
        # children
        parent = elem.getparent()
        if top:
            parent.remove(elem)
        else:
            index = parent.index(elem)
            parent[index:index+1] = list(elem.iterchildren())

    # Tree 1
    hit_split_point = False
    keep_descendants = False
    split_point_descendants = frozenset(split_point.iterdescendants())
    for elem in tuple(body.iterdescendants()):
        if elem is split_point:
            hit_split_point = True
            if before:
                nix_element(elem)
            else:
                # We want to keep the descendants of the split point in
                # Tree 1
                keep_descendants = True
                # We want the split point element, but not its tail
                elem.tail = '\n'

            continue
        if hit_split_point:
            if keep_descendants:
                if elem in split_point_descendants:
                    # elem is a descendant keep it
                    continue
                else:
                    # We are out of split_point, so prevent further set
                    # lookups of split_point_descendants
                    keep_descendants = False
            nix_element(elem)

    # Tree 2
    ancestors = frozenset(XPath('ancestor::*')(split_point2))
    for elem in tuple(body2.iterdescendants()):
        if elem is split_point2:
            if not before:
                # Keep the split point element's tail, if it contains non-whitespace
                # text
                tail = elem.tail
                if tail and not tail.isspace():
                    parent = elem.getparent()
                    idx = parent.index(elem)
                    if idx == 0:
                        parent.text = (parent.text or '') + tail
                    else:
                        sib = parent[idx-1]
                        sib.tail = (sib.tail or '') + tail
                # Remove the element itself
                nix_element(elem)
            break
        if elem in ancestors:
            # We have to preserve the ancestors as they could have CSS
            # styles that are inherited/applicable, like font or
            # width. So we only remove the text, if any.
            elem.text = '\n'
        else:
            nix_element(elem, top=False)

    body2.text = '\n'

    return tree, tree2

class SplitLinkReplacer:

    def __init__(self, base, bottom_anchors, top_name, bottom_name, container):
        self.bottom_anchors, self.bottom_name = bottom_anchors, bottom_name
        self.container, self.top_name = container, top_name
        self.base = base
        self.replaced = False

    def __call__(self, url):
        if url and url.startswith('#'):
            return url
        name = self.container.href_to_name(url, self.base)
        if name != self.top_name:
            return url
        purl = urlparse(url)
        if purl.fragment and purl.fragment in self.bottom_anchors:
            url = self.container.name_to_href(self.bottom_name, self.base) + '#' + purl.fragment
            self.replaced = True
        return url

[documentos] def split(container, name, loc_or_xpath, before=True, totals=None): ''' Split the file specified by name at the position specified by loc_or_xpath. Splitting automatically migrates all links and references to the affected files. :param loc_or_xpath: Should be an XPath expression such as //h:div[@id="split_here"]. Can also be a *loc* which is used internally to implement splitting in the preview panel. :param before: If True the split occurs before the identified element otherwise after it. :param totals: Used internally ''' root = container.parsed(name) if isinstance(loc_or_xpath, str): split_point = root.xpath(loc_or_xpath)[0] else: try: split_point = node_from_loc(root, loc_or_xpath, totals=totals) except MalformedMarkup: # The webkit HTML parser and the container parser have yielded # different node counts, this can happen if the file is valid XML # but contains constructs like nested <p> tags. So force parse it # with the HTML 5 parser and try again. raw = container.raw_data(name) root = container.parse_xhtml(raw, fname=name, force_html5_parse=True) try: split_point = node_from_loc(root, loc_or_xpath, totals=totals) except MalformedMarkup: raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool' ' before splitting') % name) container.replace(name, root) if in_table(split_point): raise AbortError('Cannot split inside tables') if split_point.tag.endswith('}body'): raise AbortError('Cannot split on the <body> tag') tree1, tree2 = do_split(split_point, container.log, before=before) root1, root2 = tree1.getroot(), tree2.getroot() anchors_in_top = frozenset(root1.xpath('//*/@id')) | frozenset(root1.xpath('//*/@name')) | {''} anchors_in_bottom = frozenset(root2.xpath('//*/@id')) | frozenset(root2.xpath('//*/@name')) base, ext = name.rpartition('.')[0::2] base = re.sub(r'_split\d+$', '', base) nname, s = None, 0 while not nname or container.exists(nname): s += 1 nname = '%s_split%d.%s' % (base, s, ext) manifest_item = container.generate_item(nname, media_type=container.mime_map[name]) bottom_name = container.href_to_name(manifest_item.get('href'), container.opf_name) # Fix links in the split trees for r in (root1, root2): for a in r.xpath('//*[@href]'): url = a.get('href') if url.startswith('#'): fname = name else: fname = container.href_to_name(url, name) if fname == name: purl = urlparse(url) if purl.fragment in anchors_in_top: if r is root2: a.set('href', f'{container.name_to_href(name, bottom_name)}#{purl.fragment}') else: a.set('href', '#' + purl.fragment) elif purl.fragment in anchors_in_bottom: if r is root1: a.set('href', f'{container.name_to_href(bottom_name, name)}#{purl.fragment}') else: a.set('href', '#' + purl.fragment) # Fix all links in the container that point to anchors in the bottom tree for fname, media_type in iteritems(container.mime_map): if fname not in {name, bottom_name}: repl = SplitLinkReplacer(fname, anchors_in_bottom, name, bottom_name, container) container.replace_links(fname, repl) container.replace(name, root1) container.replace(bottom_name, root2) spine = container.opf_xpath('//opf:spine')[0] for spine_item, spine_name, linear in container.spine_iter: if spine_name == name: break index = spine.index(spine_item) + 1 si = spine.makeelement(OPF('itemref'), idref=manifest_item.get('id')) if not linear: si.set('linear', 'no') container.insert_into_xml(spine, si, index=index) container.dirty(container.opf_name) return bottom_name

[documentos] def multisplit(container, name, xpath, before=True): ''' Split the specified file at multiple locations (all tags that match the specified XPath expression). See also: :func:`split`. Splitting automatically migrates all links and references to the affected files. :param before: If True the splits occur before the identified element otherwise after it. ''' root = container.parsed(name) nodes = root.xpath(xpath, namespaces=XPNSMAP) if not nodes: raise AbortError(_('The expression %s did not match any nodes') % xpath) for split_point in nodes: if in_table(split_point): raise AbortError('Cannot split inside tables') if split_point.tag.endswith('}body'): raise AbortError('Cannot split on the <body> tag') for i, tag in enumerate(nodes): tag.set('calibre-split-point', str(i)) current = name all_names = [name] for i in range(len(nodes)): current = split(container, current, '//*[@calibre-split-point="%d"]' % i, before=before) all_names.append(current) for x in all_names: for tag in container.parsed(x).xpath('//*[@calibre-split-point]'): tag.attrib.pop('calibre-split-point') container.dirty(x) return all_names[1:]

class MergeLinkReplacer:

    def __init__(self, base, anchor_map, master, container):
        self.container, self.anchor_map = container, anchor_map
        self.master = master
        self.base = base
        self.replaced = False

    def __call__(self, url):
        if url and url.startswith('#'):
            return url
        name = self.container.href_to_name(url, self.base)
        amap = self.anchor_map.get(name, None)
        if amap is None:
            return url
        purl = urlparse(url)
        frag = purl.fragment or ''
        frag = amap.get(frag, frag)
        url = self.container.name_to_href(self.master, self.base) + '#' + frag
        self.replaced = True
        return url

def add_text(body, text):
    if len(body) > 0:
        body[-1].tail = (body[-1].tail or '') + text
    else:
        body.text = (body.text or '') + text

def all_anchors(root):
    return set(root.xpath('//*/@id')) | set(root.xpath('//*/@name'))

def all_stylesheets(container, name):
    for link in XPath('//h:head/h:link[@href]')(container.parsed(name)):
        name = container.href_to_name(link.get('href'), name)
        typ = link.get('type', 'text/css')
        if typ == 'text/css':
            yield name

def unique_anchor(seen_anchors, current):
    c = 0
    ans = current
    while ans in seen_anchors:
        c += 1
        ans = '%s_%d' % (current, c)
    return ans

def remove_name_attributes(root):
    # Remove all name attributes, replacing them with id attributes
    for elem in root.xpath('//*[@id and @name]'):
        del elem.attrib['name']
    for elem in root.xpath('//*[@name]'):
        elem.set('id', elem.attrib.pop('name'))

def merge_html(container, names, master, insert_page_breaks=False):
    p = container.parsed
    root = p(master)

    # Ensure master has a <head>
    head = root.find('h:head', namespaces=XPNSMAP)
    if head is None:
        head = root.makeelement(XHTML('head'))
        container.insert_into_xml(root, head, 0)

    seen_anchors = all_anchors(root)
    seen_stylesheets = set(all_stylesheets(container, master))
    master_body = p(master).findall('h:body', namespaces=XPNSMAP)[-1]
    master_base = os.path.dirname(master)
    anchor_map = {n:{} for n in names if n != master}
    first_anchor_map = {}

    for name in names:
        if name == master:
            continue
        # Insert new stylesheets into master
        for sheet in all_stylesheets(container, name):
            if sheet not in seen_stylesheets:
                seen_stylesheets.add(sheet)
                link = head.makeelement(XHTML('link'), rel='stylesheet', type='text/css', href=container.name_to_href(sheet, master))
                container.insert_into_xml(head, link)

        # Rebase links if master is in a different directory
        if os.path.dirname(name) != master_base:
            container.replace_links(name, LinkRebaser(container, name, master))

        root = p(name)
        children = []
        for body in p(name).findall('h:body', namespaces=XPNSMAP):
            children.append(body.text if body.text and body.text.strip() else '\n\n')
            children.extend(body)

        first_child = ''
        for first_child in children:
            if not isinstance(first_child, string_or_bytes):
                break
        if isinstance(first_child, string_or_bytes):
            # body contained only text, no tags
            first_child = body.makeelement(XHTML('p'))
            first_child.text, children[0] = children[0], first_child

        amap = anchor_map[name]
        remove_name_attributes(root)

        for elem in root.xpath('//*[@id]'):
            val = elem.get('id')
            if not val:
                continue
            if val in seen_anchors:
                nval = unique_anchor(seen_anchors, val)
                elem.set('id', nval)
                amap[val] = nval
            else:
                seen_anchors.add(val)

        if 'id' not in first_child.attrib:
            first_child.set('id', unique_anchor(seen_anchors, 'top'))
            seen_anchors.add(first_child.get('id'))
        first_anchor_map[name] = first_child.get('id')

        if insert_page_breaks:
            first_child.set('style', first_child.get('style', '') + '; page-break-before: always')

        amap[''] = first_child.get('id')

        # Fix links that point to local changed anchors
        for a in XPath('//h:a[starts-with(@href, "#")]')(root):
            q = a.get('href')[1:]
            if q in amap:
                a.set('href', '#' + amap[q])

        for child in children:
            if isinstance(child, string_or_bytes):
                add_text(master_body, child)
            else:
                master_body.append(copy.deepcopy(child))

        container.remove_item(name, remove_from_guide=False)

    # Fix all links in the container that point to merged files
    for fname, media_type in iteritems(container.mime_map):
        repl = MergeLinkReplacer(fname, anchor_map, master, container)
        container.replace_links(fname, repl)

    return first_anchor_map

def merge_css(container, names, master):
    p = container.parsed
    msheet = p(master)
    master_base = os.path.dirname(master)
    merged = set()

    for name in names:
        if name == master:
            continue
        # Rebase links if master is in a different directory
        if os.path.dirname(name) != master_base:
            container.replace_links(name, LinkRebaser(container, name, master))

        sheet = p(name)

        # Remove charset rules
        cr = [r for r in sheet.cssRules if r.type == r.CHARSET_RULE]
        [sheet.deleteRule(sheet.cssRules.index(r)) for r in cr]
        for rule in sheet.cssRules:
            msheet.add(rule)

        container.remove_item(name)
        merged.add(name)

    # Remove links to merged stylesheets in the html files, replacing with a
    # link to the master sheet
    for name, mt in iteritems(container.mime_map):
        if mt in OEB_DOCS:
            removed = False
            root = p(name)
            for link in XPath('//h:link[@href]')(root):
                q = container.href_to_name(link.get('href'), name)
                if q in merged:
                    container.remove_from_xml(link)
                    removed = True
            if removed:
                container.dirty(name)
            if removed and master not in set(all_stylesheets(container, name)):
                head = root.find('h:head', namespaces=XPNSMAP)
                if head is not None:
                    link = head.makeelement(XHTML('link'), type='text/css', rel='stylesheet', href=container.name_to_href(master, name))
                    container.insert_into_xml(head, link)

[documentos] def merge(container, category, names, master): ''' Merge the specified files into a single file, automatically migrating all links and references to the affected files. The file must all either be HTML or CSS files. :param category: Must be either ``'text'`` for HTML files or ``'styles'`` for CSS files :param names: The list of files to be merged :param master: Which of the merged files is the *master* file, that is, the file that will remain after merging. ''' if category not in {'text', 'styles'}: raise AbortError('Cannot merge files of type: %s' % category) if len(names) < 2: raise AbortError('Must specify at least two files to be merged') if master not in names: raise AbortError('The master file (%s) must be one of the files being merged' % master) if category == 'text': merge_html(container, names, master) elif category == 'styles': merge_css(container, names, master) container.dirty(master)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.ebooks.oeb.polish.toc

Código fuente para calibre.ebooks.oeb.polish.toc

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import re
from collections import Counter, OrderedDict
from functools import partial
from operator import itemgetter

from lxml import etree
from lxml.builder import ElementMaker

from calibre import __version__
from calibre.ebooks.oeb.base import EPUB_NS, NCX, NCX_NS, OEB_DOCS, XHTML, XHTML_NS, XML, XML_NS, XPath, serialize, uuid_id, xml2text
from calibre.ebooks.oeb.polish.errors import MalformedMarkup
from calibre.ebooks.oeb.polish.opf import get_book_language, set_guide_item
from calibre.ebooks.oeb.polish.pretty import pretty_html_tree, pretty_xml_tree
from calibre.ebooks.oeb.polish.utils import extract, guess_type
from calibre.translations.dynamic import translate
from calibre.utils.localization import canonicalize_lang, get_lang, lang_as_iso639_1
from calibre.utils.resources import get_path as P
from polyglot.builtins import iteritems
from polyglot.urllib import urlparse

ns = etree.FunctionNamespace('calibre_xpath_extensions')
ns.prefix = 'calibre'
ns['lower-case'] = lambda c, x: x.lower() if hasattr(x, 'lower') else x

class TOC:

    toc_title = None

    def __init__(self, title=None, dest=None, frag=None):
        self.title, self.dest, self.frag = title, dest, frag
        self.dest_exists = self.dest_error = None
        if self.title:
            self.title = self.title.strip()
        self.parent = None
        self.children = []
        self.page_list = []

    def add(self, title, dest, frag=None):
        c = TOC(title, dest, frag)
        self.children.append(c)
        c.parent = self
        return c

    def remove(self, child):
        self.children.remove(child)
        child.parent = None

    def remove_from_parent(self):
        if self.parent is None:
            return
        idx = self.parent.children.index(self)
        for child in reversed(self.children):
            child.parent = self.parent
            self.parent.children.insert(idx, child)
        self.parent.children.remove(self)
        self.parent = None

    def __iter__(self):
        yield from self.children

    def __len__(self):
        return len(self.children)

    def iterdescendants(self, level=None):
        gc_level = None if level is None else level + 1
        for child in self:
            if level is None:
                yield child
            else:
                yield level, child
            yield from child.iterdescendants(level=gc_level)

    def remove_duplicates(self, only_text=True):
        seen = set()
        remove = []
        for child in self:
            key = child.title if only_text else (child.title, child.dest, (child.frag or None))
            if key in seen:
                remove.append(child)
            else:
                seen.add(key)
                child.remove_duplicates()
        for child in remove:
            self.remove(child)

    @property
    def depth(self):
        """The maximum depth of the navigation tree rooted at this node."""
        try:
            return max(node.depth for node in self) + 1
        except ValueError:
            return 1

    @property
    def last_child(self):
        return self.children[-1] if self.children else None

    def get_lines(self, lvl=0):
        frag = ('#'+self.frag) if self.frag else ''
        ans = [('\t'*lvl) + 'TOC: %s --> %s%s'%(self.title, self.dest, frag)]
        for child in self:
            ans.extend(child.get_lines(lvl+1))
        return ans

    def __str__(self):
        return '\n'.join(self.get_lines())

    def to_dict(self, node_counter=None):
        ans = {
            'title':self.title, 'dest':self.dest, 'frag':self.frag,
            'children':[c.to_dict(node_counter) for c in self.children]
        }
        if self.dest_exists is not None:
            ans['dest_exists'] = self.dest_exists
        if self.dest_error is not None:
            ans['dest_error'] = self.dest_error
        if node_counter is not None:
            ans['id'] = next(node_counter)
        return ans

    @property
    def as_dict(self):
        return self.to_dict()

def child_xpath(tag, name):
    return tag.xpath('./*[calibre:lower-case(local-name()) = "%s"]'%name)

def add_from_navpoint(container, navpoint, parent, ncx_name):
    dest = frag = text = None
    nl = child_xpath(navpoint, 'navlabel')
    if nl:
        nl = nl[0]
        text = ''
        for txt in child_xpath(nl, 'text'):
            text += etree.tostring(txt, method='text',
                    encoding='unicode', with_tail=False)
    content = child_xpath(navpoint, 'content')
    if content:
        content = content[0]
        href = content.get('src', None)
        if href:
            dest = container.href_to_name(href, base=ncx_name)
            frag = urlparse(href).fragment or None
    return parent.add(text or None, dest or None, frag or None)

def process_ncx_node(container, node, toc_parent, ncx_name):
    for navpoint in node.xpath('./*[calibre:lower-case(local-name()) = "navpoint"]'):
        child = add_from_navpoint(container, navpoint, toc_parent, ncx_name)
        if child is not None:
            process_ncx_node(container, navpoint, child, ncx_name)

def parse_ncx(container, ncx_name):
    root = container.parsed(ncx_name)
    toc_root = TOC()
    navmaps = root.xpath('//*[calibre:lower-case(local-name()) = "navmap"]')
    if navmaps:
        process_ncx_node(container, navmaps[0], toc_root, ncx_name)
    toc_root.lang = toc_root.uid = None
    for attr, val in iteritems(root.attrib):
        if attr.endswith('lang'):
            toc_root.lang = str(val)
            break
    for uid in root.xpath('//*[calibre:lower-case(local-name()) = "meta" and @name="dtb:uid"]/@content'):
        if uid:
            toc_root.uid = str(uid)
            break
    for pl in root.xpath('//*[calibre:lower-case(local-name()) = "pagelist"]'):
        for pt in pl.xpath('descendant::*[calibre:lower-case(local-name()) = "pagetarget"]'):
            pagenum = pt.get('value')
            if pagenum:
                href = pt.xpath('descendant::*[calibre:lower-case(local-name()) = "content"]/@src')
                if href:
                    dest = container.href_to_name(href[0], base=ncx_name)
                    frag = urlparse(href[0]).fragment or None
                    toc_root.page_list.append({'dest': dest, 'pagenum': pagenum, 'frag': frag})
    return toc_root

def add_from_li(container, li, parent, nav_name):
    dest = frag = text = None
    for x in li.iterchildren(XHTML('a'), XHTML('span')):
        text = etree.tostring(x, method='text', encoding='unicode', with_tail=False).strip() or ' '.join(x.xpath('descendant-or-self::*/@title')).strip()
        href = x.get('href')
        if href:
            dest = nav_name if href.startswith('#') else container.href_to_name(href, base=nav_name)
            frag = urlparse(href).fragment or None
        break
    return parent.add(text or None, dest or None, frag or None)

def first_child(parent, tagname):
    try:
        return next(parent.iterchildren(tagname))
    except StopIteration:
        return None

def process_nav_node(container, node, toc_parent, nav_name):
    for li in node.iterchildren(XHTML('li')):
        child = add_from_li(container, li, toc_parent, nav_name)
        ol = first_child(li, XHTML('ol'))
        if child is not None and ol is not None:
            process_nav_node(container, ol, child, nav_name)

def parse_nav(container, nav_name):
    root = container.parsed(nav_name)
    toc_root = TOC()
    toc_root.lang = toc_root.uid = None
    et = '{%s}type' % EPUB_NS
    for nav in root.iterdescendants(XHTML('nav')):
        if nav.get(et) == 'toc':
            ol = first_child(nav, XHTML('ol'))
            if ol is not None:
                process_nav_node(container, ol, toc_root, nav_name)
                for h in nav.iterchildren(*map(XHTML, 'h1 h2 h3 h4 h5 h6'.split())):
                    text = etree.tostring(h, method='text', encoding='unicode', with_tail=False) or h.get('title')
                    if text:
                        toc_root.toc_title = text
                        break
                break
    return toc_root

def verify_toc_destinations(container, toc):
    anchor_map = {}
    anchor_xpath = XPath('//*/@id|//h:a/@name')
    for item in toc.iterdescendants():
        name = item.dest
        if not name:
            item.dest_exists = False
            item.dest_error = _('No file named %s exists')%name
            continue
        try:
            root = container.parsed(name)
        except KeyError:
            item.dest_exists = False
            item.dest_error = _('No file named %s exists')%name
            continue
        if not hasattr(root, 'xpath'):
            item.dest_exists = False
            item.dest_error = _('No HTML file named %s exists')%name
            continue
        if not item.frag:
            item.dest_exists = True
            continue
        if name not in anchor_map:
            anchor_map[name] = frozenset(anchor_xpath(root))
        item.dest_exists = item.frag in anchor_map[name]
        if not item.dest_exists:
            item.dest_error = _(
                'The anchor %(a)s does not exist in file %(f)s')%dict(
                a=item.frag, f=name)

def find_existing_ncx_toc(container):
    toc = container.opf_xpath('//opf:spine/@toc')
    if toc:
        toc = container.manifest_id_map.get(toc[0], None)
    if not toc:
        ncx = guess_type('a.ncx')
        toc = container.manifest_type_map.get(ncx, [None])[0]
    return toc or None

def find_existing_nav_toc(container):
    for name in container.manifest_items_with_property('nav'):
        return name

def mark_as_nav(container, name):
    if container.opf_version_parsed.major > 2:
        container.apply_unique_properties(name, 'nav')

def get_x_toc(container, find_toc, parse_toc, verify_destinations=True):
    def empty_toc():
        ans = TOC()
        ans.lang = ans.uid = None
        return ans
    toc = find_toc(container)
    ans = empty_toc() if toc is None or not container.has_name(toc) else parse_toc(container, toc)
    ans.toc_file_name = toc if toc and container.has_name(toc) else None
    if verify_destinations:
        verify_toc_destinations(container, ans)
    return ans

def get_toc(container, verify_destinations=True):
    ver = container.opf_version_parsed
    if ver.major < 3:
        return get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
    else:
        ans = get_x_toc(container, find_existing_nav_toc, parse_nav, verify_destinations=verify_destinations)
        if len(ans) == 0:
            ans = get_x_toc(container, find_existing_ncx_toc, parse_ncx, verify_destinations=verify_destinations)
        return ans

def get_guide_landmarks(container):
    for ref in container.opf_xpath('./opf:guide/opf:reference'):
        href, title, rtype = ref.get('href'), ref.get('title'), ref.get('type')
        href, frag = href.partition('#')[::2]
        name = container.href_to_name(href, container.opf_name)
        if container.has_name(name):
            yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}

def get_nav_landmarks(container):
    nav = find_existing_nav_toc(container)
    if nav and container.has_name(nav):
        root = container.parsed(nav)
        et = '{%s}type' % EPUB_NS
        for elem in root.iterdescendants(XHTML('nav')):
            if elem.get(et) == 'landmarks':
                for li in elem.iterdescendants(XHTML('li')):
                    for a in li.iterdescendants(XHTML('a')):
                        href, rtype = a.get('href'), a.get(et)
                        if href:
                            title = etree.tostring(a, method='text', encoding='unicode', with_tail=False).strip()
                            href, frag = href.partition('#')[::2]
                            name = container.href_to_name(href, nav)
                            if container.has_name(name):
                                yield {'dest':name, 'frag':frag, 'title':title or '', 'type':rtype or ''}
                            break

def get_landmarks(container):
    ver = container.opf_version_parsed
    if ver.major < 3:
        return list(get_guide_landmarks(container))
    ans = list(get_nav_landmarks(container))
    if len(ans) == 0:
        ans = list(get_guide_landmarks(container))
    return ans

def ensure_id(elem, all_ids):
    elem_id = elem.get('id')
    if elem_id:
        return False, elem_id
    if elem.tag == XHTML('a'):
        anchor = elem.get('name', None)
        if anchor:
            elem.set('id', anchor)
            return False, anchor
    c = 0
    while True:
        c += 1
        q = f'toc_{c}'
        if q not in all_ids:
            elem.set('id', q)
            all_ids.add(q)
            break
    return True, elem.get('id')

def elem_to_toc_text(elem, prefer_title=False):
    text = xml2text(elem).strip()
    if prefer_title:
        text = elem.get('title', '').strip() or text
    if not text:
        text = elem.get('title', '')
    if not text:
        text = elem.get('alt', '')
    text = re.sub(r'\s+', ' ', text.strip())
    text = text[:1000].strip()
    if not text:
        text = _('(Untitled)')
    return text

def item_at_top(elem):
    try:
        body = XPath('//h:body')(elem.getroottree().getroot())[0]
    except (TypeError, IndexError, KeyError, AttributeError):
        return False
    tree = body.getroottree()
    path = tree.getpath(elem)
    for el in body.iterdescendants(etree.Element):
        epath = tree.getpath(el)
        if epath == path:
            break
        try:
            if el.tag.endswith('}img') or (el.text and el.text.strip()):
                return False
        except:
            return False
        if not path.startswith(epath):
            # Only check tail of non-parent elements
            if el.tail and el.tail.strip():
                return False
    return True

[documentos] def from_xpaths(container, xpaths, prefer_title=False): ''' Generate a Table of Contents from a list of XPath expressions. Each expression in the list corresponds to a level of the generate ToC. For example: :code:`['//h:h1', '//h:h2', '//h:h3']` will generate a three level Table of Contents from the ``<h1>``, ``<h2>`` and ``<h3>`` tags. ''' tocroot = TOC() xpaths = [XPath(xp) for xp in xpaths] # Find those levels that have no elements in all spine items maps = OrderedDict() empty_levels = {i+1 for i, xp in enumerate(xpaths)} for spinepath in container.spine_items: name = container.abspath_to_name(spinepath) root = container.parsed(name) level_item_map = maps[name] = {i+1:frozenset(xp(root)) for i, xp in enumerate(xpaths)} for lvl, elems in iteritems(level_item_map): if elems: empty_levels.discard(lvl) # Remove empty levels from all level_maps if empty_levels: for name, lmap in tuple(iteritems(maps)): lmap = {lvl:items for lvl, items in iteritems(lmap) if lvl not in empty_levels} lmap = sorted(iteritems(lmap), key=itemgetter(0)) lmap = {i+1:items for i, (l, items) in enumerate(lmap)} maps[name] = lmap node_level_map = {tocroot: 0} def parent_for_level(child_level): limit = child_level - 1 def process_node(node): child = node.last_child if child is None: return node lvl = node_level_map[child] return node if lvl > limit else child if lvl == limit else process_node(child) return process_node(tocroot) for name, level_item_map in iteritems(maps): root = container.parsed(name) item_level_map = {e:i for i, elems in iteritems(level_item_map) for e in elems} item_dirtied = False all_ids = set(root.xpath('//*/@id')) for item in root.iterdescendants(etree.Element): lvl = item_level_map.get(item, None) if lvl is None: continue text = elem_to_toc_text(item, prefer_title) parent = parent_for_level(lvl) if item_at_top(item): dirtied, elem_id = False, None else: dirtied, elem_id = ensure_id(item, all_ids) item_dirtied = dirtied or item_dirtied toc = parent.add(text, name, elem_id) node_level_map[toc] = lvl toc.dest_exists = True if item_dirtied: container.commit_item(name, keep_parsed=True) return tocroot

[documentos] def from_links(container): ''' Generate a Table of Contents from links in the book. ''' toc = TOC() link_path = XPath('//h:a[@href]') seen_titles, seen_dests = set(), set() for name, is_linear in container.spine_names: root = container.parsed(name) for a in link_path(root): href = a.get('href') if not href or not href.strip(): continue frag = None if href.startswith('#'): dest = name frag = href[1:] else: href, _, frag = href.partition('#') dest = container.href_to_name(href, base=name) frag = frag or None if (dest, frag) in seen_dests: continue seen_dests.add((dest, frag)) text = elem_to_toc_text(a) if text in seen_titles: continue seen_titles.add(text) toc.add(text, dest, frag=frag) verify_toc_destinations(container, toc) for child in toc: if not child.dest_exists: toc.remove(child) return toc

def find_text(node):
    LIMIT = 200
    pat = re.compile(r'\s+')
    for child in node:
        if isinstance(child, etree._Element):
            text = xml2text(child).strip()
            text = pat.sub(' ', text)
            if len(text) < 1:
                continue
            if len(text) > LIMIT:
                # Look for less text in a child of this node, recursively
                ntext = find_text(child)
                return ntext or (text[:LIMIT] + '...')
            else:
                return text

[documentos] def from_files(container): ''' Generate a Table of Contents from files in the book. ''' toc = TOC() for i, spinepath in enumerate(container.spine_items): name = container.abspath_to_name(spinepath) root = container.parsed(name) body = XPath('//h:body')(root) if not body: continue text = find_text(body[0]) if not text: text = name.rpartition('/')[-1] if i == 0 and text.rpartition('.')[0].lower() in {'titlepage', 'cover'}: text = _('Cover') toc.add(text, name) return toc

def node_from_loc(root, locs, totals=None):
    node = root.xpath('//*[local-name()="body"]')[0]
    for i, loc in enumerate(locs):
        children = tuple(node.iterchildren(etree.Element))
        if totals is not None and totals[i] != len(children):
            raise MalformedMarkup()
        node = children[loc]
    return node

def add_id(container, name, loc, totals=None):
    root = container.parsed(name)
    try:
        node = node_from_loc(root, loc, totals=totals)
    except MalformedMarkup:
        # The webkit HTML parser and the container parser have yielded
        # different node counts, this can happen if the file is valid XML
        # but contains constructs like nested <p> tags. So force parse it
        # with the HTML 5 parser and try again.
        raw = container.raw_data(name)
        root = container.parse_xhtml(raw, fname=name, force_html5_parse=True)
        try:
            node = node_from_loc(root, loc, totals=totals)
        except MalformedMarkup:
            raise MalformedMarkup(_('The file %s has malformed markup. Try running the Fix HTML tool'
                                    ' before editing.') % name)
        container.replace(name, root)

    if not node.get('id'):
        ensure_id(node, set(root.xpath('//*/@id')))
    container.commit_item(name, keep_parsed=True)
    return node.get('id')

def create_ncx(toc, to_href, btitle, lang, uid):
    lang = lang.replace('_', '-')
    ncx = etree.Element(NCX('ncx'),
        attrib={'version': '2005-1', XML('lang'): lang},
        nsmap={None: NCX_NS})
    head = etree.SubElement(ncx, NCX('head'))
    etree.SubElement(head, NCX('meta'),
        name='dtb:uid', content=str(uid))
    etree.SubElement(head, NCX('meta'),
        name='dtb:depth', content=str(toc.depth))
    generator = ''.join(['calibre (', __version__, ')'])
    etree.SubElement(head, NCX('meta'),
        name='dtb:generator', content=generator)
    etree.SubElement(head, NCX('meta'), name='dtb:totalPageCount', content='0')
    etree.SubElement(head, NCX('meta'), name='dtb:maxPageNumber', content='0')
    title = etree.SubElement(ncx, NCX('docTitle'))
    text = etree.SubElement(title, NCX('text'))
    text.text = btitle
    navmap = etree.SubElement(ncx, NCX('navMap'))
    spat = re.compile(r'\s+')

    play_order = Counter()

    def process_node(xml_parent, toc_parent):
        for child in toc_parent:
            play_order['c'] += 1
            point = etree.SubElement(xml_parent, NCX('navPoint'), id='num_%d' % play_order['c'],
                            playOrder=str(play_order['c']))
            label = etree.SubElement(point, NCX('navLabel'))
            title = child.title
            if title:
                title = spat.sub(' ', title)
            etree.SubElement(label, NCX('text')).text = title
            if child.dest:
                href = to_href(child.dest)
                if child.frag:
                    href += '#'+child.frag
                etree.SubElement(point, NCX('content'), src=href)
            process_node(point, child)

    process_node(navmap, toc)
    return ncx

def commit_ncx_toc(container, toc, lang=None, uid=None):
    tocname = find_existing_ncx_toc(container)
    if tocname is None:
        item = container.generate_item('toc.ncx', id_prefix='toc')
        tocname = container.href_to_name(item.get('href'), base=container.opf_name)
        ncx_id = item.get('id')
        [s.set('toc', ncx_id) for s in container.opf_xpath('//opf:spine')]
    if not lang:
        lang = get_lang()
        for l in container.opf_xpath('//dc:language'):
            l = canonicalize_lang(xml2text(l).strip())
            if l:
                lang = l
                lang = lang_as_iso639_1(l) or l
                break
    lang = lang_as_iso639_1(lang) or lang
    if not uid:
        uid = uuid_id()
        eid = container.opf.get('unique-identifier', None)
        if eid:
            m = container.opf_xpath('//*[@id="%s"]'%eid)
            if m:
                uid = xml2text(m[0])

    title = _('Table of Contents')
    m = container.opf_xpath('//dc:title')
    if m:
        x = xml2text(m[0]).strip()
        title = x or title

    to_href = partial(container.name_to_href, base=tocname)
    root = create_ncx(toc, to_href, title, lang, uid)
    container.replace(tocname, root)
    container.pretty_print.add(tocname)

def ensure_single_nav_of_type(root, ntype='toc'):
    et = '{%s}type' % EPUB_NS
    navs = [n for n in root.iterdescendants(XHTML('nav')) if n.get(et) == ntype]
    for x in navs[1:]:
        extract(x)
    if navs:
        nav = navs[0]
        tail = nav.tail
        attrib = dict(nav.attrib)
        nav.clear()
        nav.attrib.update(attrib)
        nav.tail = tail
    else:
        nav = root.makeelement(XHTML('nav'))
        first_child(root, XHTML('body')).append(nav)
    nav.set('{%s}type' % EPUB_NS, ntype)
    return nav

def ensure_container_has_nav(container, lang=None, previous_nav=None):
    tocname = find_existing_nav_toc(container)
    if previous_nav is not None:
        nav_name = container.href_to_name(previous_nav[0])
        if nav_name and container.exists(nav_name):
            tocname = nav_name
            container.apply_unique_properties(tocname, 'nav')
    if tocname is None:
        item = container.generate_item('nav.xhtml', id_prefix='nav')
        item.set('properties', 'nav')
        tocname = container.href_to_name(item.get('href'), base=container.opf_name)
        if previous_nav is not None:
            root = previous_nav[1]
        else:
            root = container.parse_xhtml(P('templates/new_nav.html', data=True).decode('utf-8'))
        container.replace(tocname, root)
    else:
        root = container.parsed(tocname)
    if lang:
        lang = lang_as_iso639_1(lang) or lang
        root.set('lang', lang)
        root.set('{%s}lang' % XML_NS, lang)
    return tocname, root

def collapse_li(parent):
    for li in parent.iterdescendants(XHTML('li')):
        if len(li) == 1:
            li.text = None
            li[0].tail = None

def create_nav_li(container, ol, entry, tocname):
    li = ol.makeelement(XHTML('li'))
    ol.append(li)
    a = li.makeelement(XHTML('a'))
    li.append(a)
    href = container.name_to_href(entry['dest'], tocname)
    if entry['frag']:
        href += '#' + entry['frag']
    a.set('href', href)
    return a

def set_landmarks(container, root, tocname, landmarks):
    nav = ensure_single_nav_of_type(root, 'landmarks')
    nav.set('hidden', '')
    ol = nav.makeelement(XHTML('ol'))
    nav.append(ol)
    for entry in landmarks:
        if entry['type'] and container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
            a = create_nav_li(container, ol, entry, tocname)
            a.set('{%s}type' % EPUB_NS, entry['type'])
            a.text = entry['title'] or None
    pretty_xml_tree(nav)
    collapse_li(nav)

def commit_nav_toc(container, toc, lang=None, landmarks=None, previous_nav=None):
    tocname, root = ensure_container_has_nav(container, lang=lang, previous_nav=previous_nav)
    nav = ensure_single_nav_of_type(root, 'toc')
    if toc.toc_title:
        nav.append(nav.makeelement(XHTML('h1')))
        nav[-1].text = toc.toc_title

    rnode = nav.makeelement(XHTML('ol'))
    nav.append(rnode)
    to_href = partial(container.name_to_href, base=tocname)
    spat = re.compile(r'\s+')

    def process_node(xml_parent, toc_parent):
        for child in toc_parent:
            li = xml_parent.makeelement(XHTML('li'))
            xml_parent.append(li)
            title = child.title or ''
            title = spat.sub(' ', title).strip()
            a = li.makeelement(XHTML('a' if child.dest else 'span'))
            a.text = title
            li.append(a)
            if child.dest:
                href = to_href(child.dest)
                if child.frag:
                    href += '#'+child.frag
                a.set('href', href)
            if len(child):
                ol = li.makeelement(XHTML('ol'))
                li.append(ol)
                process_node(ol, child)
    process_node(rnode, toc)
    pretty_xml_tree(nav)

    collapse_li(nav)
    nav.tail = '\n'

    if toc.page_list:
        nav = ensure_single_nav_of_type(root, 'page-list')
        nav.set('hidden', '')
        ol = nav.makeelement(XHTML('ol'))
        nav.append(ol)
        for entry in toc.page_list:
            if container.has_name(entry['dest']) and container.mime_map[entry['dest']] in OEB_DOCS:
                a = create_nav_li(container, ol, entry, tocname)
                a.text = str(entry['pagenum'])
        pretty_xml_tree(nav)
        collapse_li(nav)
    container.replace(tocname, root)

def commit_toc(container, toc, lang=None, uid=None):
    commit_ncx_toc(container, toc, lang=lang, uid=uid)
    if container.opf_version_parsed.major > 2:
        commit_nav_toc(container, toc, lang=lang)

def remove_names_from_toc(container, names):
    changed = []
    names = frozenset(names)
    for find_toc, parse_toc, commit_toc in (
            (find_existing_ncx_toc, parse_ncx, commit_ncx_toc),
            (find_existing_nav_toc, parse_nav, commit_nav_toc),
    ):
        toc = get_x_toc(container, find_toc, parse_toc, verify_destinations=False)
        if len(toc) > 0:
            remove = []
            for node in toc.iterdescendants():
                if node.dest in names:
                    remove.append(node)
            if remove:
                for node in reversed(remove):
                    node.remove_from_parent()
                commit_toc(container, toc)
                changed.append(find_toc(container))
    return changed

def find_inline_toc(container):
    for name, linear in container.spine_names:
        if container.parsed(name).xpath('//*[local-name()="body" and @id="calibre_generated_inline_toc"]'):
            return name

def toc_to_html(toc, container, toc_name, title, lang=None):

    def process_node(html_parent, toc, level=1, indent='  ', style_level=2):
        li = html_parent.makeelement(XHTML('li'))
        li.tail = '\n'+ (indent*level)
        html_parent.append(li)
        name, frag = toc.dest, toc.frag
        href = '#'
        if name:
            href = container.name_to_href(name, toc_name)
            if frag:
                href += '#' + frag
        a = li.makeelement(XHTML('a'), href=href)
        a.text = toc.title
        li.append(a)
        if len(toc) > 0:
            parent = li.makeelement(XHTML('ul'))
            parent.set('class', 'level%d' % (style_level))
            li.append(parent)
            a.tail = '\n\n' + (indent*(level+2))
            parent.text = '\n'+(indent*(level+3))
            parent.tail = '\n\n' + (indent*(level+1))
            for child in toc:
                process_node(parent, child, level+3, style_level=style_level + 1)
            parent[-1].tail = '\n' + (indent*(level+2))

    E = ElementMaker(namespace=XHTML_NS, nsmap={None:XHTML_NS})
    html = E.html(
        E.head(
            E.title(title),
            E.style(P('templates/inline_toc_styles.css', data=True).decode('utf-8'), type='text/css'),
        ),
        E.body(
            E.h2(title),
            E.ul(),
            id="calibre_generated_inline_toc",
        )
    )

    ul = html[1][1]
    ul.set('class', 'level1')
    for child in toc:
        process_node(ul, child)
    if lang:
        html.set('lang', lang)
    pretty_html_tree(container, html)
    return html

[documentos] def create_inline_toc(container, title=None): ''' Create an inline (HTML) Table of Contents from an existing NCX Table of Contents. :param title: The title for this table of contents. ''' lang = get_book_language(container) default_title = 'Table of Contents' if lang: lang = lang_as_iso639_1(lang) or lang default_title = translate(lang, default_title) title = title or default_title toc = get_toc(container) if len(toc) == 0: return None toc_name = find_inline_toc(container) name = toc_name html = toc_to_html(toc, container, name, title, lang) raw = serialize(html, 'text/html') if name is None: name, c = 'toc.xhtml', 0 while container.has_name(name): c += 1 name = 'toc%d.xhtml' % c container.add_file(name, raw, spine_index=0) else: with container.open(name, 'wb') as f: f.write(raw) set_guide_item(container, 'toc', title, name, frag='calibre_generated_inline_toc') return name

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.gui2.actions

Código fuente para calibre.gui2.actions

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

from functools import partial
from zipfile import ZipFile

from qt.core import QAction, QIcon, QKeySequence, QMenu, QObject, QPoint, QTimer, QToolButton

from calibre import prints
from calibre.constants import ismacos
from calibre.gui2 import Dispatcher
from calibre.gui2.keyboard import NameConflict
from polyglot.builtins import string_or_bytes

def toolbar_widgets_for_action(gui, action):
    # Search the toolbars for the widget associated with an action, passing
    # them to the caller for further processing
    for x in gui.bars_manager.bars:
        try:
            w = x.widgetForAction(action)
            # It seems that multiple copies of the action can exist, such as
            # when the device-connected menu is changed while the device is
            # connected. Use the one that has an actual position.
            if w is None or w.pos().x() == 0:
                continue
            # The button might be hidden
            if not w.isVisible():
                continue
            yield(w)
        except Exception:
            continue

def show_menu_under_widget(gui, menu, action, name):
    # First try the tool bar
    for w in toolbar_widgets_for_action(gui, action):
        try:
            # The w.height() assures that the menu opens below the button.
            menu.exec(w.mapToGlobal(QPoint(0, w.height())))
            return
        except Exception:
            continue
    # Now try the menu bar
    for x in gui.bars_manager.menu_bar.added_actions:
        # This depends on no two menus with the same name.
        # I don't know if this works on a Mac
        if x.text() == name:
            try:
                # The menu item might be hidden
                if not x.isVisible():
                    continue
                # We can't use x.trigger() because it doesn't put the menu
                # in the right place. Instead get the position of the menu
                # widget on the menu bar
                p = x.parent().menu_bar
                r = p.actionGeometry(x)
                # Make sure that the menu item is actually displayed in the menu
                # and not the overflow
                if p.geometry().width() < (r.x() + r.width()):
                    continue
                # Show the menu under the name in the menu bar
                menu.exec(p.mapToGlobal(QPoint(r.x()+2, r.height()-2)))
                return
            except Exception:
                continue
    # No visible button found. Fall back to displaying in upper left corner
    # of the library view.
    menu.exec(gui.library_view.mapToGlobal(QPoint(10, 10)))

def menu_action_unique_name(plugin, unique_name):
    return '%s : menu action : %s'%(plugin.unique_name, unique_name)

[documentos] class InterfaceAction(QObject): ''' A plugin representing an "action" that can be taken in the graphical user interface. All the items in the toolbar and context menus are implemented by these plugins. Note that this class is the base class for these plugins, however, to integrate the plugin with calibre's plugin system, you have to make a wrapper class that references the actual plugin. See the :mod:`calibre.customize.builtins` module for examples. If two :class:`InterfaceAction` objects have the same name, the one with higher priority takes precedence. Sub-classes should implement the :meth:`genesis`, :meth:`library_changed`, :meth:`location_selected`, :meth:`shutting_down`, :meth:`initialization_complete` and :meth:`tag_browser_context_action` methods. Once initialized, this plugin has access to the main calibre GUI via the :attr:`gui` member. You can access other plugins by name, for example:: self.gui.iactions['Save To Disk'] To access the actual plugin, use the :attr:`interface_action_base_plugin` attribute, this attribute only becomes available after the plugin has been initialized. Useful if you want to use methods from the plugin class like do_user_config(). The QAction specified by :attr:`action_spec` is automatically create and made available as ``self.qaction``. ''' #: The plugin name. If two plugins with the same name are present, the one #: with higher priority takes precedence. name = 'Implement me' #: The plugin priority. If two plugins with the same name are present, the one #: with higher priority takes precedence. priority = 1 #: The menu popup type for when this plugin is added to a toolbar popup_type = QToolButton.ToolButtonPopupMode.MenuButtonPopup #: Whether this action should be auto repeated when its shortcut #: key is held down. auto_repeat = False #: Of the form: (text, icon_path, tooltip, keyboard shortcut). #: icon, tooltip and keyboard shortcut can be None. #: keyboard shortcut must be either a string, None or tuple of shortcuts. #: If None, a keyboard shortcut corresponding to the action is not #: registered. If you pass an empty tuple, then the shortcut is registered #: with no default key binding. action_spec = ('text', 'icon', None, None) #: If not None, used for the name displayed to the user when customizing #: the keyboard shortcuts for the above action spec instead of action_spec[0] action_shortcut_name = None #: If True, a menu is automatically created and added to self.qaction action_add_menu = False #: If True, a clone of self.qaction is added to the menu of self.qaction #: If you want the text of this action to be different from that of #: self.qaction, set this variable to the new text action_menu_clone_qaction = False #: Set of locations to which this action must not be added. #: See :attr:`all_locations` for a list of possible locations dont_add_to = frozenset() #: Set of locations from which this action must not be removed. #: See :attr:`all_locations` for a list of possible locations dont_remove_from = frozenset() all_locations = frozenset(['toolbar', 'toolbar-device', 'context-menu', 'context-menu-device', 'toolbar-child', 'menubar', 'menubar-device', 'context-menu-cover-browser', 'context-menu-split', 'searchbar']) #: Type of action #: 'current' means acts on the current view #: 'global' means an action that does not act on the current view, but rather #: on calibre as a whole action_type = 'global' #: If True, then this InterfaceAction will have the opportunity to interact #: with drag and drop events. See the methods, :meth:`accept_enter_event`, #: :meth`:accept_drag_move_event`, :meth:`drop_event` for details. accepts_drops = False def __init__(self, parent, site_customization): QObject.__init__(self, parent) self.setObjectName(self.name) self.gui = parent self.site_customization = site_customization self.interface_action_base_plugin = None

[documentos] def accept_enter_event(self, event, mime_data): ''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.''' return False

[documentos] def accept_drag_move_event(self, event, mime_data): ''' This method should return True iff this interface action is capable of handling the drag event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI.''' return False

[documentos] def drop_event(self, event, mime_data): ''' This method should perform some useful action and return True iff this interface action is capable of handling the drop event. Do not call accept/ignore on the event, that will be taken care of by the calibre UI. You should not perform blocking/long operations in this function. Instead emit a signal or use QTimer.singleShot and return quickly. See the builtin actions for examples.''' return False

def do_genesis(self): self.Dispatcher = partial(Dispatcher, parent=self) self.create_action() self.gui.addAction(self.qaction) self.gui.addAction(self.menuless_qaction) self.genesis() self.location_selected('library') @property def unique_name(self): bn = self.__class__.__name__ if getattr(self.interface_action_base_plugin, 'name'): bn = self.interface_action_base_plugin.name return 'Interface Action: %s (%s)'%(bn, self.name) def create_action(self, spec=None, attr='qaction', shortcut_name=None, persist_shortcut=False): if spec is None: spec = self.action_spec text, icon, tooltip, shortcut = spec if icon is not None: action = QAction(QIcon.ic(icon), text, self.gui) else: action = QAction(text, self.gui) if attr == 'qaction': if hasattr(self.action_menu_clone_qaction, 'rstrip'): mt = str(self.action_menu_clone_qaction) else: mt = action.text() self.menuless_qaction = ma = QAction(action.icon(), mt, self.gui) ma.triggered.connect(action.trigger) for a in ((action, ma) if attr == 'qaction' else (action,)): a.setAutoRepeat(self.auto_repeat) text = tooltip if tooltip else text a.setToolTip(text) a.setStatusTip(text) a.setWhatsThis(text) shortcut_action = action desc = tooltip if tooltip else None if attr == 'qaction': shortcut_action = ma if shortcut is not None: keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else tuple(shortcut)) if shortcut_name is None: if self.action_shortcut_name is not None: shortcut_name = self.action_shortcut_name elif spec[0]: shortcut_name = str(spec[0]) if shortcut_name and self.action_spec[0] and not ( attr == 'qaction' and self.popup_type == QToolButton.ToolButtonPopupMode.InstantPopup): try: self.gui.keyboard.register_shortcut(self.unique_name + ' - ' + attr, shortcut_name, default_keys=keys, action=shortcut_action, description=desc, group=self.action_spec[0], persist_shortcut=persist_shortcut) except NameConflict as e: try: prints(str(e)) except: pass shortcut_action.setShortcuts([QKeySequence(key, QKeySequence.SequenceFormat.PortableText) for key in keys]) else: self.shortcut_action_for_context_menu = shortcut_action if ismacos: # In Qt 5 keyboard shortcuts dont work unless the # action is explicitly added to the main window self.gui.addAction(shortcut_action) if attr is not None: setattr(self, attr, action) if attr == 'qaction' and self.action_add_menu: menu = QMenu() action.setMenu(menu) if self.action_menu_clone_qaction: menu.addAction(self.menuless_qaction) return action

[documentos] def create_menu_action(self, menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False): ''' Convenience method to easily add actions to a QMenu. Returns the created QAction. This action has one extra attribute calibre_shortcut_unique_name which if not None refers to the unique name under which this action is registered with the keyboard manager. :param menu: The QMenu the newly created action will be added to :param unique_name: A unique name for this action, this must be globally unique, so make it as descriptive as possible. If in doubt, add an UUID to it. :param text: The text of the action. :param icon: Either a QIcon or a file name. The file name is passed to the QIcon.ic() builtin, so you do not need to pass the full path to the images folder. :param shortcut: A string, a list of strings, None or False. If False, no keyboard shortcut is registered for this action. If None, a keyboard shortcut with no default keybinding is registered. String and list of strings register a shortcut with default keybinding as specified. :param description: A description for this action. Used to set tooltips. :param triggered: A callable which is connected to the triggered signal of the created action. :param shortcut_name: The text displayed to the user when customizing the keyboard shortcuts for this action. By default it is set to the value of ``text``. :param persist_shortcut: Shortcuts for actions that don't always appear, or are library dependent, may disappear when other keyboard shortcuts are edited unless ```persist_shortcut``` is set True. ''' if shortcut_name is None: shortcut_name = str(text) ac = menu.addAction(text) if icon is not None: if not isinstance(icon, QIcon): icon = QIcon.ic(icon) ac.setIcon(icon) keys = () if shortcut is not None and shortcut is not False: keys = ((shortcut,) if isinstance(shortcut, string_or_bytes) else tuple(shortcut)) unique_name = menu_action_unique_name(self, unique_name) if description is not None: ac.setToolTip(description) ac.setStatusTip(description) ac.setWhatsThis(description) ac.calibre_shortcut_unique_name = unique_name if shortcut is not False: self.gui.keyboard.register_shortcut(unique_name, shortcut_name, default_keys=keys, action=ac, description=description, group=self.action_spec[0], persist_shortcut=persist_shortcut) # In Qt 5 keyboard shortcuts dont work unless the # action is explicitly added to the main window and on OSX and # Unity since the menu might be exported, the shortcuts won't work self.gui.addAction(ac) if triggered is not None: ac.triggered.connect(triggered) return ac

[documentos] def load_resources(self, names): ''' If this plugin comes in a ZIP file (user added plugin), this method will allow you to load resources from the ZIP file. For example to load an image:: pixmap = QPixmap() pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0]) icon = QIcon(pixmap) :param names: List of paths to resources in the ZIP file using / as separator :return: A dictionary of the form ``{name : file_contents}``. Any names that were not found in the ZIP file will not be present in the dictionary. ''' if self.plugin_path is None: raise ValueError('This plugin was not loaded from a ZIP file') ans = {} with ZipFile(self.plugin_path, 'r') as zf: for candidate in zf.namelist(): if candidate in names: ans[candidate] = zf.read(candidate) return ans

[documentos] def genesis(self): ''' Setup this plugin. Only called once during initialization. self.gui is available. The action specified by :attr:`action_spec` is available as ``self.qaction``. ''' pass

[documentos] def location_selected(self, loc): ''' Called whenever the book list being displayed in calibre changes. Currently values for loc are: ``library, main, card and cardb``. This method should enable/disable this action and its sub actions as appropriate for the location. ''' pass

[documentos] def library_about_to_change(self, olddb, db): ''' Called whenever the current library is changed. :param olddb: The LibraryDatabase corresponding to the previous library. :param db: The LibraryDatabase corresponding to the new library. ''' pass

[documentos] def library_changed(self, db): ''' Called whenever the current library is changed. :param db: The LibraryDatabase corresponding to the current library. ''' pass

[documentos] def gui_layout_complete(self): ''' Called once per action when the layout of the main GUI is completed. If your action needs to make changes to the layout, they should be done here, rather than in :meth:`initialization_complete`. ''' pass

[documentos] def initialization_complete(self): ''' Called once per action when the initialization of the main GUI is completed. ''' pass

[documentos] def tag_browser_context_action(self, index): ''' Called when displaying the context menu in the Tag browser. ``index`` is the QModelIndex that points to the Tag browser item that was right clicked. Test it for validity with index.valid() and get the underlying TagTreeItem object with index.data(Qt.ItemDataRole.UserRole). Any action objects yielded by this method will be added to the context menu. ''' if False: yield QAction()

[documentos] def shutting_down(self): ''' Called once per plugin when the main GUI is in the process of shutting down. Release any used resources, but try not to block the shutdown for long periods of time. ''' pass

class InterfaceActionWithLibraryDrop(InterfaceAction):
    '''
    Subclass of InterfaceAction that implemente methods to execute the default action
    by drop some books from the library.

    Inside the do_drop() method, the ids of the droped books are provided
    by the attribute self.dropped_ids
    '''

    accepts_drops = True
    mimetype_for_drop = 'application/calibre+from_library'

    def accept_enter_event(self, event, mime_data):
        if mime_data.hasFormat(self.mimetype_for_drop):
            return True
        return False

    def accept_drag_move_event(self, event, mime_data):
        if mime_data.hasFormat(self.mimetype_for_drop):
            return True
        return False

    def drop_event(self, event, mime_data):
        if mime_data.hasFormat(self.mimetype_for_drop):
            self.dropped_ids = tuple(map(int, mime_data.data(self.mimetype_for_drop).data().split()))
            QTimer.singleShot(1, self.do_drop)
            return True
        return False

    def do_drop(self):
        raise NotImplementedError()

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.gui2.preferences

Código fuente para calibre.gui2.preferences

#!/usr/bin/env python

__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import textwrap

from qt.core import (
    QAbstractSpinBox,
    QApplication,
    QCheckBox,
    QComboBox,
    QDialog,
    QDialogButtonBox,
    QEvent,
    QIcon,
    QLineEdit,
    QListView,
    QListWidget,
    Qt,
    QTableWidget,
    QVBoxLayout,
    QWidget,
    pyqtSignal,
)

from calibre.customize.ui import preferences_plugins
from calibre.gui2.complete2 import EditWithComplete
from calibre.gui2.widgets import HistoryLineEdit
from calibre.utils.config import ConfigProxy
from polyglot.builtins import string_or_bytes

class AbortCommit(Exception):
    pass

class AbortInitialize(Exception):
    pass

[documentos] class ConfigWidgetInterface: ''' This class defines the interface that all widgets displayed in the Preferences dialog must implement. See :class:`ConfigWidgetBase` for a base class that implements this interface and defines various convenience methods as well. ''' #: This signal must be emitted whenever the user changes a value in this #: widget changed_signal = None #: Set to True iff the :meth:`restore_to_defaults` method is implemented. supports_restoring_to_defaults = True #: The tooltip for the "Restore defaults" button restore_defaults_desc = _('Restore settings to default values. ' 'You have to click Apply to actually save the default settings.') #: If True the Preferences dialog will not allow the user to set any more #: preferences. Only has effect if :meth:`commit` returns True. restart_critical = False

[documentos] def genesis(self, gui): ''' Called once before the widget is displayed, should perform any necessary setup. :param gui: The main calibre graphical user interface ''' raise NotImplementedError()

[documentos] def initialize(self): ''' Should set all config values to their initial values (the values stored in the config files). A "return" statement is optional. Return False if the dialog is not to be shown. ''' raise NotImplementedError()

[documentos] def restore_defaults(self): ''' Should set all config values to their defaults. ''' pass

[documentos] def commit(self): ''' Save any changed settings. Return True if the changes require a restart, False otherwise. Raise an :class:`AbortCommit` exception to indicate that an error occurred. You are responsible for giving the user feedback about what the error is and how to correct it. ''' return False

[documentos] def refresh_gui(self, gui): ''' Called once after this widget is committed. Responsible for causing the gui to reread any changed settings. Note that by default the GUI re-initializes various elements anyway, so most widgets won't need to use this method. ''' pass

[documentos] def initial_tab_changed(self): ''' Called if the initially displayed tab is changed before the widget is shown, but after it is initialized. ''' pass

def set_help_tips(gui_obj, tt):
    if tt:
        if not str(gui_obj.whatsThis()):
            gui_obj.setWhatsThis(tt)
        if not str(gui_obj.statusTip()):
            gui_obj.setStatusTip(tt)
        tt = '\n'.join(textwrap.wrap(tt, 70))
        gui_obj.setToolTip(tt)

class Setting:

    CHOICES_SEARCH_FLAGS = Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive

    def __init__(self, name, config_obj, widget, gui_name=None,
            empty_string_is_None=True, choices=None, restart_required=False):
        self.name, self.gui_name = name, gui_name
        self.empty_string_is_None = empty_string_is_None
        self.restart_required = restart_required
        self.choices = choices
        if gui_name is None:
            self.gui_name = 'opt_'+name
        self.config_obj = config_obj
        self.gui_obj = getattr(widget, self.gui_name)
        self.widget = widget

        if isinstance(self.gui_obj, QCheckBox):
            self.datatype = 'bool'
            self.gui_obj.stateChanged.connect(self.changed)
        elif isinstance(self.gui_obj, QAbstractSpinBox):
            self.datatype = 'number'
            self.gui_obj.valueChanged.connect(self.changed)
        elif isinstance(self.gui_obj, (QLineEdit, HistoryLineEdit)):
            self.datatype = 'string'
            self.gui_obj.textChanged.connect(self.changed)
            if isinstance(self.gui_obj, HistoryLineEdit):
                self.gui_obj.initialize('preferences_setting_' + self.name)
        elif isinstance(self.gui_obj, QComboBox):
            self.datatype = 'choice'
            self.gui_obj.editTextChanged.connect(self.changed)
            self.gui_obj.currentIndexChanged.connect(self.changed)
        else:
            raise ValueError('Unknown data type %s' % self.gui_obj.__class__)

        if isinstance(self.config_obj, ConfigProxy) and \
                not str(self.gui_obj.toolTip()):
            h = self.config_obj.help(self.name)
            if h:
                self.gui_obj.setToolTip(h)
        tt = str(self.gui_obj.toolTip())
        set_help_tips(self.gui_obj, tt)

    def changed(self, *args):
        self.widget.changed_signal.emit()

    def initialize(self):
        self.gui_obj.blockSignals(True)
        if self.datatype == 'choice':
            choices = self.choices or []
            if isinstance(self.gui_obj, EditWithComplete):
                self.gui_obj.all_items = choices
            else:
                self.gui_obj.clear()
                for x in choices:
                    if isinstance(x, string_or_bytes):
                        x = (x, x)
                    self.gui_obj.addItem(x[0], (x[1]))
        self.set_gui_val(self.get_config_val(default=False))
        self.gui_obj.blockSignals(False)
        self.initial_value = self.get_gui_val()

    def commit(self):
        val = self.get_gui_val()
        oldval = self.get_config_val()
        changed = val != oldval
        if changed:
            self.set_config_val(self.get_gui_val())
        return changed and self.restart_required

    def restore_defaults(self):
        self.set_gui_val(self.get_config_val(default=True))

    def get_config_val(self, default=False):
        if default:
            val = self.config_obj.defaults[self.name]
        else:
            val = self.config_obj[self.name]
        return val

    def set_config_val(self, val):
        self.config_obj[self.name] = val

    def set_gui_val(self, val):
        if self.datatype == 'bool':
            self.gui_obj.setChecked(bool(val))
        elif self.datatype == 'number':
            self.gui_obj.setValue(val)
        elif self.datatype == 'string':
            self.gui_obj.setText(val if val else '')
        elif self.datatype == 'choice':
            if isinstance(self.gui_obj, EditWithComplete):
                self.gui_obj.setText(val)
            else:
                idx = self.gui_obj.findData((val), role=Qt.ItemDataRole.UserRole,
                        flags=self.CHOICES_SEARCH_FLAGS)
                if idx == -1:
                    idx = 0
                self.gui_obj.setCurrentIndex(idx)

    def get_gui_val(self):
        if self.datatype == 'bool':
            val = bool(self.gui_obj.isChecked())
        elif self.datatype == 'number':
            val = self.gui_obj.value()
        elif self.datatype == 'string':
            val = str(self.gui_obj.text()).strip()
            if self.empty_string_is_None and not val:
                val = None
        elif self.datatype == 'choice':
            if isinstance(self.gui_obj, EditWithComplete):
                val = str(self.gui_obj.text())
            else:
                idx = self.gui_obj.currentIndex()
                if idx < 0:
                    idx = 0
                val = str(self.gui_obj.itemData(idx) or '')
        return val

class CommaSeparatedList(Setting):

    def set_gui_val(self, val):
        x = ''
        if val:
            x = ', '.join(val)
        self.gui_obj.setText(x)

    def get_gui_val(self):
        val = str(self.gui_obj.text()).strip()
        ans = []
        if val:
            ans = [x.strip() for x in val.split(',')]
            ans = [x for x in ans if x]
        return ans

[documentos] class ConfigWidgetBase(QWidget, ConfigWidgetInterface): ''' Base class that contains code to easily add standard config widgets like checkboxes, combo boxes, text fields and so on. See the :meth:`register` method. This class automatically handles change notification, resetting to default, translation between gui objects and config objects, etc. for registered settings. If your config widget inherits from this class but includes setting that are not registered, you should override the :class:`ConfigWidgetInterface` methods and call the base class methods inside the overrides. ''' changed_signal = pyqtSignal() restart_now = pyqtSignal() supports_restoring_to_defaults = True restart_critical = False def __init__(self, parent=None): QWidget.__init__(self, parent) if hasattr(self, 'setupUi'): self.setupUi(self) self.settings = {}

[documentos] def register(self, name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=Setting): ''' Register a setting. :param name: The setting name :param config_obj: The config object that reads/writes the setting :param gui_name: The name of the GUI object that presents an interface to change the setting. By default it is assumed to be ``'opt_' + name``. :param choices: If this setting is a multiple choice (combobox) based setting, the list of choices. The list is a list of two element tuples of the form: ``[(gui name, value), ...]`` :param setting: The class responsible for managing this setting. The default class handles almost all cases, so this param is rarely used. ''' setting = setting(name, config_obj, self, gui_name=gui_name, choices=choices, restart_required=restart_required, empty_string_is_None=empty_string_is_None) return self.register_setting(setting)

def register_setting(self, setting): self.settings[setting.name] = setting return setting

[documentos] def initialize(self): for setting in self.settings.values(): setting.initialize()

[documentos] def commit(self, *args): restart_required = False for setting in self.settings.values(): rr = setting.commit() if rr: restart_required = True return restart_required

[documentos] def restore_defaults(self, *args): for setting in self.settings.values(): setting.restore_defaults()

def get_plugin(category, name):
    for plugin in preferences_plugins():
        if plugin.category == category and plugin.name == name:
            return plugin
    raise ValueError(
            'No Preferences Plugin with category: %s and name: %s found' %
            (category, name))

class ConfigDialog(QDialog):

    def set_widget(self, w):
        self.w = w

    def accept(self):
        try:
            self.restart_required = self.w.commit()
        except AbortCommit:
            return
        QDialog.accept(self)

def init_gui():
    from calibre.gui2.main import option_parser
    from calibre.gui2.ui import Main
    from calibre.library import db
    parser = option_parser()
    opts, args = parser.parse_args([])
    actions = tuple(Main.create_application_menubar())
    db = db()
    gui = Main(opts)
    gui.initialize(db.library_path, db, actions, show_gui=False)
    return gui

def show_config_widget(category, name, gui=None, show_restart_msg=False,
        parent=None, never_shutdown=False):
    '''
    Show the preferences plugin identified by category and name

    :param gui: gui instance, if None a hidden gui is created
    :param show_restart_msg: If True and the preferences plugin indicates a
    restart is required, show a message box telling the user to restart
    :param parent: The parent of the displayed dialog

    :return: True iff a restart is required for the changes made by the user to
    take effect
    '''
    from calibre.gui2 import gprefs
    pl = get_plugin(category, name)
    d = ConfigDialog(parent)
    d.resize(750, 550)
    conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name)
    d.setWindowTitle(_('Configure ') + pl.gui_name)
    d.setWindowIcon(QIcon.ic('config.png'))
    bb = QDialogButtonBox(d)
    bb.setStandardButtons(QDialogButtonBox.StandardButton.Apply|QDialogButtonBox.StandardButton.Cancel|QDialogButtonBox.StandardButton.RestoreDefaults)
    bb.accepted.connect(d.accept)
    bb.rejected.connect(d.reject)
    w = pl.create_widget(d)
    d.set_widget(w)
    bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).clicked.connect(w.restore_defaults)
    bb.button(QDialogButtonBox.StandardButton.RestoreDefaults).setEnabled(w.supports_restoring_to_defaults)
    bb.button(QDialogButtonBox.StandardButton.Apply).setEnabled(False)
    bb.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(d.accept)

    def onchange():
        b = bb.button(QDialogButtonBox.StandardButton.Apply)
        b.setEnabled(True)
        b.setDefault(True)
        b.setAutoDefault(True)
    w.changed_signal.connect(onchange)
    bb.button(QDialogButtonBox.StandardButton.Cancel).setFocus(Qt.FocusReason.OtherFocusReason)
    l = QVBoxLayout()
    d.setLayout(l)
    l.addWidget(w)
    l.addWidget(bb)
    mygui = gui is None
    if gui is None:
        gui = init_gui()
        mygui = True
    w.genesis(gui)
    w.initialize()
    d.restore_geometry(gprefs, conf_name)
    d.exec()
    d.save_geometry(gprefs, conf_name)
    rr = getattr(d, 'restart_required', False)
    if show_restart_msg and rr:
        from calibre.gui2 import warning_dialog
        warning_dialog(gui, 'Restart required', 'Restart required', show=True)
    if mygui and not never_shutdown:
        gui.shutdown()
    return rr

class ListViewWithMoveByKeyPress(QListView):

    def set_movement_functions(self, up_function, down_function):
        self.up_function = up_function
        self.down_function = down_function

    def event(self, event):
        if (event.type() == QEvent.KeyPress and
            QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
            if event.key() == Qt.Key.Key_Up:
                self.up_function()
            elif event.key() == Qt.Key.Key_Down:
                self.down_function()
            return True
        return QListView.event(self, event)

class ListWidgetWithMoveByKeyPress(QListWidget):

    def set_movement_functions(self, up_function, down_function):
        self.up_function = up_function
        self.down_function = down_function

    def event(self, event):
        if (event.type() == QEvent.KeyPress and
            QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
            if event.key() == Qt.Key.Key_Up:
                self.up_function()
            elif event.key() == Qt.Key.Key_Down:
                self.down_function()
            return True
        return QListWidget.event(self, event)

class TableWidgetWithMoveByKeyPress(QTableWidget):

    def set_movement_functions(self, up_function, down_function):
        self.up_function = up_function
        self.down_function = down_function

    def event(self, event):
        if (event.type() == QEvent.KeyPress and
            QApplication.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier):
            if event.key() == Qt.Key.Key_Up:
                self.up_function()
            elif event.key() == Qt.Key.Key_Down:
                self.down_function()
            return True
        return QTableWidget.event(self, event)

# Testing {{{

def test_widget(category, name, gui=None):
    show_config_widget(category, name, gui=gui, show_restart_msg=True)

def test_all():
    from qt.core import QApplication
    app = QApplication([])
    app
    gui = init_gui()
    for plugin in preferences_plugins():
        test_widget(plugin.category, plugin.name, gui=gui)
    gui.shutdown()

if __name__ == '__main__':
    test_all()
# }}}

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.gui2.tweak_book.boss

Código fuente para calibre.gui2.tweak_book.boss

#!/usr/bin/env python
# License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>

import errno
import os
import shutil
import subprocess
import sys
import tempfile
from functools import partial, wraps

from qt.core import (
    QApplication,
    QCheckBox,
    QDialog,
    QDialogButtonBox,
    QGridLayout,
    QIcon,
    QInputDialog,
    QLabel,
    QMimeData,
    QObject,
    QSize,
    Qt,
    QTimer,
    QUrl,
    QVBoxLayout,
    pyqtSignal,
)

from calibre import isbytestring, prints
from calibre.constants import cache_dir, islinux, ismacos, iswindows
from calibre.ebooks.oeb.base import urlnormalize
from calibre.ebooks.oeb.polish.container import OEB_DOCS, OEB_STYLES, clone_container, guess_type
from calibre.ebooks.oeb.polish.container import get_container as _gc
from calibre.ebooks.oeb.polish.cover import mark_as_cover, mark_as_titlepage, set_cover
from calibre.ebooks.oeb.polish.css import filter_css, rename_class
from calibre.ebooks.oeb.polish.main import SUPPORTED, tweak_polish
from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all
from calibre.ebooks.oeb.polish.replace import get_recommended_folders, rationalize_folders, rename_files, replace_file
from calibre.ebooks.oeb.polish.split import AbortError, merge, multisplit, split
from calibre.ebooks.oeb.polish.toc import create_inline_toc, mark_as_nav, remove_names_from_toc
from calibre.ebooks.oeb.polish.utils import link_stylesheets
from calibre.ebooks.oeb.polish.utils import setup_css_parser_serialization as scs
from calibre.gui2 import (
    add_to_recent_docs,
    choose_dir,
    choose_files,
    choose_save_file,
    error_dialog,
    info_dialog,
    open_url,
    question_dialog,
    sanitize_env_vars,
    warning_dialog,
)
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.tweak_book import actions, current_container, dictionaries, editor_name, editors, set_book_locale, set_current_container, tprefs
from calibre.gui2.tweak_book.completion.worker import completion_worker
from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime
from calibre.gui2.tweak_book.editor.insert_resource import NewBook, get_resource_data
from calibre.gui2.tweak_book.file_list import FILE_COPY_MIME, NewFileDialog
from calibre.gui2.tweak_book.preferences import Preferences
from calibre.gui2.tweak_book.preview import parse_worker
from calibre.gui2.tweak_book.save import SaveManager, find_first_existing_ancestor, save_container
from calibre.gui2.tweak_book.search import run_search, validate_search_request
from calibre.gui2.tweak_book.spell import find_next as find_next_word
from calibre.gui2.tweak_book.spell import find_next_error
from calibre.gui2.tweak_book.toc import TOCEditor
from calibre.gui2.tweak_book.undo import GlobalUndoHistory
from calibre.gui2.tweak_book.widgets import (
    AddCover,
    FilterCSS,
    ImportForeign,
    InsertLink,
    InsertSemantics,
    InsertTag,
    MultiSplit,
    QuickOpen,
    RationalizeFolders,
)
from calibre.gui2.widgets import BusyCursor
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory
from calibre.startup import connect_lambda
from calibre.utils.config import JSONConfig
from calibre.utils.icu import numeric_sort_key
from calibre.utils.imghdr import identify
from calibre.utils.ipc.launch import exe_path, macos_edit_book_bundle_path
from calibre.utils.localization import ngettext
from calibre.utils.tdir_in_cache import tdir_in_cache
from polyglot.builtins import as_bytes, iteritems, itervalues, string_or_bytes
from polyglot.urllib import urlparse

_diff_dialogs = []
last_used_transform_rules = []
last_used_html_transform_rules = []

def get_container(*args, **kwargs):
    kwargs['tweak_mode'] = True
    container = _gc(*args, **kwargs)
    return container

def setup_css_parser_serialization():
    scs(tprefs['editor_tab_stop_width'])

def in_thread_job(func):
    @wraps(func)
    def ans(*args, **kwargs):
        with BusyCursor():
            return func(*args, **kwargs)
    return ans

def get_boss():
    return get_boss.boss

[documentos] class Boss(QObject): handle_completion_result_signal = pyqtSignal(object) def __init__(self, parent, notify=None): QObject.__init__(self, parent) self.global_undo = GlobalUndoHistory() self.file_was_readonly = False self.container_count = 0 self.tdir = None self.save_manager = SaveManager(parent, notify) self.save_manager.report_error.connect(self.report_save_error) self.save_manager.check_for_completion.connect(self.check_terminal_save) self.doing_terminal_save = False self.ignore_preview_to_editor_sync = False setup_css_parser_serialization() get_boss.boss = self self.gui = parent completion_worker().result_callback = self.handle_completion_result_signal.emit self.handle_completion_result_signal.connect(self.handle_completion_result, Qt.ConnectionType.QueuedConnection) self.completion_request_count = 0 self.editor_cache = JSONConfig('editor-cache', base_path=cache_dir()) d = self.editor_cache.defaults d['edit_book_state'] = {} d['edit_book_state_order'] = [] def __call__(self, gui): self.gui = gui gui.message_popup.undo_requested.connect(self.do_global_undo) fl = gui.file_list fl.delete_requested.connect(self.delete_requested) fl.reorder_spine.connect(self.reorder_spine) fl.rename_requested.connect(self.rename_requested) fl.bulk_rename_requested.connect(self.bulk_rename_requested) fl.edit_file.connect(self.edit_file_requested) fl.merge_requested.connect(self.merge_requested) fl.mark_requested.connect(self.mark_requested) fl.export_requested.connect(self.export_requested) fl.replace_requested.connect(self.replace_requested) fl.link_stylesheets_requested.connect(self.link_stylesheets_requested) fl.initiate_file_copy.connect(self.copy_files_to_clipboard) fl.initiate_file_paste.connect(self.paste_files_from_clipboard) fl.open_file_with.connect(self.open_file_with) self.gui.central.current_editor_changed.connect(self.apply_current_editor_state) self.gui.central.close_requested.connect(self.editor_close_requested) self.gui.central.search_panel.search_triggered.connect(self.search) self.gui.text_search.find_text.connect(self.find_text) self.gui.preview.sync_requested.connect(self.sync_editor_to_preview) self.gui.preview.split_start_requested.connect(self.split_start_requested) self.gui.preview.split_requested.connect(self.split_requested) self.gui.preview.link_clicked.connect(self.link_clicked) self.gui.preview.render_process_restarted.connect(self.report_render_process_restart) self.gui.preview.open_file_with.connect(self.open_file_with) self.gui.preview.edit_file.connect(self.edit_file_requested) self.gui.check_book.item_activated.connect(self.check_item_activated) self.gui.check_book.check_requested.connect(self.check_requested) self.gui.check_book.fix_requested.connect(self.fix_requested) self.gui.toc_view.navigate_requested.connect(self.link_clicked) self.gui.toc_view.refresh_requested.connect(self.commit_all_editors_to_container) self.gui.image_browser.image_activated.connect(self.image_activated) self.gui.checkpoints.revert_requested.connect(self.revert_requested) self.gui.checkpoints.compare_requested.connect(self.compare_requested) self.gui.saved_searches.run_saved_searches.connect(self.run_saved_searches) self.gui.saved_searches.copy_search_to_search_panel.connect(self.gui.central.search_panel.paste_saved_search) self.gui.central.search_panel.save_search.connect(self.save_search) self.gui.central.search_panel.show_saved_searches.connect(self.show_saved_searches) self.gui.spell_check.find_word.connect(self.find_word) self.gui.spell_check.refresh_requested.connect(self.commit_all_editors_to_container) self.gui.spell_check.word_replaced.connect(self.word_replaced) self.gui.spell_check.word_ignored.connect(self.word_ignored) self.gui.spell_check.change_requested.connect(self.word_change_requested) self.gui.live_css.goto_declaration.connect(self.goto_style_declaration) self.gui.manage_fonts.container_changed.connect(self.apply_container_update_to_gui) self.gui.manage_fonts.embed_all_fonts.connect(self.manage_fonts_embed) self.gui.manage_fonts.subset_all_fonts.connect(self.manage_fonts_subset) self.gui.reports.edit_requested.connect(self.reports_edit_requested) self.gui.reports.refresh_starting.connect(self.commit_all_editors_to_container) self.gui.reports.delete_requested.connect(self.delete_requested) def report_render_process_restart(self): self.gui.show_status_message(_('The Qt WebEngine Render process crashed and has been restarted')) @property def currently_editing(self): ' Return the name of the file being edited currently or None if no file is being edited ' return editor_name(self.gui.central.current_editor) def preferences(self): orig_spell = tprefs['inline_spell_check'] orig_size = tprefs['toolbar_icon_size'] p = Preferences(self.gui) ret = p.exec() if p.dictionaries_changed: dictionaries.clear_caches() dictionaries.initialize(force=True) # Reread user dictionaries if p.toolbars_changed: self.gui.populate_toolbars() for ed in itervalues(editors): if hasattr(ed, 'populate_toolbars'): ed.populate_toolbars() if orig_size != tprefs['toolbar_icon_size']: for ed in itervalues(editors): if hasattr(ed, 'bars'): for bar in ed.bars: bar.setIconSize(QSize(tprefs['toolbar_icon_size'], tprefs['toolbar_icon_size'])) if ret == QDialog.DialogCode.Accepted: setup_css_parser_serialization() self.gui.apply_settings() self.refresh_file_list() self.gui.preview.start_refresh_timer() if ret == QDialog.DialogCode.Accepted or p.dictionaries_changed: for ed in itervalues(editors): ed.apply_settings(dictionaries_changed=p.dictionaries_changed) if orig_spell != tprefs['inline_spell_check']: from calibre.gui2.tweak_book.editor.syntax.html import refresh_spell_check_status refresh_spell_check_status() for ed in itervalues(editors): try: ed.editor.highlighter.rehighlight() except AttributeError: pass def mark_requested(self, name, action): self.commit_dirty_opf() c = current_container() if action == 'cover': mark_as_cover(current_container(), name) elif action.startswith('titlepage:'): action, move_to_start = action.partition(':')[0::2] move_to_start = move_to_start == 'True' mark_as_titlepage(current_container(), name, move_to_start=move_to_start) elif action == 'nav': mark_as_nav(current_container(), name) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) self.gui.file_list.build(c) self.set_modified() def mkdtemp(self, prefix=''): self.container_count += 1 return tempfile.mkdtemp(prefix='%s%05d-' % (prefix, self.container_count), dir=self.tdir) def _check_before_open(self): if self.gui.action_save.isEnabled(): if not question_dialog(self.gui, _('Unsaved changes'), _( 'The current book has unsaved changes. If you open a new book, they will be lost.' ' Are you sure you want to proceed?')): return if self.save_manager.has_tasks: return info_dialog(self.gui, _('Cannot open'), _('The current book is being saved, you cannot open a new book until' ' the saving is completed'), show=True) return True def new_book(self): if not self._check_before_open(): return d = NewBook(self.gui) if d.exec() == QDialog.DialogCode.Accepted: fmt = d.fmt.lower() path = choose_save_file(self.gui, 'edit-book-new-book', _('Choose file location'), filters=[(fmt.upper(), (fmt,))], all_files=False) if path is not None: if not path.lower().endswith('.' + fmt): path = path + '.' + fmt from calibre.ebooks.oeb.polish.create import create_book create_book(d.mi, path, fmt=fmt) self.open_book(path=path) def import_book(self, path=None): if not self._check_before_open(): return d = ImportForeign(self.gui) if hasattr(path, 'rstrip'): d.set_src(os.path.abspath(path)) if d.exec() == QDialog.DialogCode.Accepted: for name in tuple(editors): self.close_editor(name) from calibre.ebooks.oeb.polish.import_book import import_book_as_epub src, dest = d.data self._clear_notify_data = True def func(src, dest, tdir): import_book_as_epub(src, dest) return get_container(dest, tdir=tdir) self.gui.blocking_job('import_book', _('Importing book, please wait...'), self.book_opened, func, src, dest, tdir=self.mkdtemp())

[documentos] def open_book(self, path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None): ''' Open the e-book at ``path`` for editing. Will show an error if the e-book is not in a supported format or the current book has unsaved changes. :param edit_file: The name of a file inside the newly opened book to start editing. Can also be a list of names. ''' if isinstance(path, (list, tuple)) and path: # Can happen from an file_event_hook on OS X when drag and dropping # onto the icon in the dock or using open -a path = path[-1] if not self._check_before_open(): return if not hasattr(path, 'rpartition'): if open_folder: path = choose_dir(self.gui, 'open-book-folder-for-tweaking', _('Choose book folder')) if path: path = [path] else: path = choose_files(self.gui, 'open-book-for-tweaking', _('Choose book'), [(_('Books'), [x.lower() for x in SUPPORTED])], all_files=False, select_only_single_file=True) if not path: return path = path[0] if not os.path.exists(path): return error_dialog(self.gui, _('File not found'), _( 'The file %s does not exist.') % path, show=True) isdir = os.path.isdir(path) ext = path.rpartition('.')[-1].upper() if ext not in SUPPORTED and not isdir: from calibre.ebooks.oeb.polish.import_book import IMPORTABLE if ext.lower() in IMPORTABLE: return self.import_book(path) return error_dialog(self.gui, _('Unsupported format'), _('Tweaking is only supported for books in the %s formats.' ' Convert your book to one of these formats first.') % _(' and ').join(sorted(SUPPORTED)), show=True) self.file_was_readonly = not os.access(path, os.W_OK) if self.file_was_readonly: warning_dialog(self.gui, _('Read-only file'), _( 'The file {} is read-only. Saving changes to it will either fail or cause its permissions to be reset.').format(path), show=True) for name in tuple(editors): self.close_editor(name) self.gui.preview.clear() self.gui.live_css.clear() self.container_count = -1 if self.tdir: shutil.rmtree(self.tdir, ignore_errors=True) # We use the cache dir rather than the temporary dir to try and prevent # temp file cleaners from nuking ebooks. See https://bugs.launchpad.net/bugs/1740460 self.tdir = tdir_in_cache('ee') self._edit_file_on_open = edit_file self._search_text_on_open = search_text self._clear_notify_data = clear_notify_data self.gui.blocking_job('open_book', _('Opening book, please wait...'), self.book_opened, get_container, path, tdir=self.mkdtemp())

def book_opened(self, job): ef = getattr(self, '_edit_file_on_open', None) cn = getattr(self, '_clear_notify_data', True) st = getattr(self, '_search_text_on_open', None) self._edit_file_on_open = self._search_text_on_open = None if job.traceback is not None: self.gui.update_status_bar_default_message() if 'DRMError:' in job.traceback: from calibre.gui2.dialogs.drm_error import DRMErrorMessage return DRMErrorMessage(self.gui).exec() if 'ObfuscationKeyMissing:' in job.traceback: return error_dialog(self.gui, _('Failed to open book'), _( 'Failed to open book, it has obfuscated fonts, but the obfuscation key is missing from the OPF.' ' Do an EPUB to EPUB conversion before trying to edit this book.'), show=True) return error_dialog(self.gui, _('Failed to open book'), _('Failed to open book, click "Show details" for more information.'), det_msg=job.traceback, show=True) if cn: self.save_manager.clear_notify_data() self.gui.check_book.clear_at_startup() self.gui.spell_check.clear_caches() dictionaries.clear_ignored(), dictionaries.clear_caches() parse_worker.clear() container = job.result set_current_container(container) completion_worker().clear_caches() with BusyCursor(): self.current_metadata = self.gui.current_metadata = container.mi lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language] set_book_locale(lang[0]) self.global_undo.open_book(container) self.gui.update_window_title() self.gui.file_list.current_edited_name = None self.gui.file_list.build(container, preserve_state=False) self.gui.action_save.setEnabled(False) self.update_global_history_actions() recent_books = list(tprefs.get('recent-books', [])) path = os.path.abspath(container.path_to_ebook) if path in recent_books: recent_books.remove(path) self.gui.update_status_bar_default_message(path) recent_books.insert(0, path) tprefs['recent-books'] = recent_books[:10] self.gui.update_recent_books() if iswindows: try: add_to_recent_docs(path) except Exception: import traceback traceback.print_exc() if ef: if isinstance(ef, str): ef = [ef] for i in ef: self.gui.file_list.request_edit(i) else: if tprefs['restore_book_state']: self.restore_book_edit_state() self.gui.toc_view.update_if_visible() self.add_savepoint(_('Start of editing session')) if st: self.find_initial_text(st) def update_editors_from_container(self, container=None, names=None): c = container or current_container() for name, ed in tuple(iteritems(editors)): if c.has_name(name): if names is None or name in names: ed.replace_data(c.raw_data(name)) ed.is_synced_to_container = True else: self.close_editor(name) def refresh_file_list(self): container = current_container() self.gui.file_list.build(container) completion_worker().clear_caches('names')

[documentos] def apply_container_update_to_gui(self, mark_as_modified=True): ''' Update all the components of the user interface to reflect the latest data in the current book container. :param mark_as_modified: If True, the book will be marked as modified, so the user will be prompted to save it when quitting. ''' self.refresh_file_list() self.update_global_history_actions() self.update_editors_from_container() if mark_as_modified: self.set_modified() self.gui.toc_view.update_if_visible() completion_worker().clear_caches() self.gui.preview.start_refresh_timer()

@in_thread_job def delete_requested(self, spine_items, other_items): self.add_savepoint(_('Before: Delete files')) self.commit_dirty_opf() c = current_container() c.remove_from_spine(spine_items) for name in other_items: c.remove_item(name) self.set_modified() self.gui.file_list.delete_done(spine_items, other_items) spine_names = [x for x, remove in spine_items if remove] completion_worker().clear_caches('names') items = spine_names + list(other_items) for name in items: if name in editors: self.close_editor(name) if not editors: self.gui.preview.clear() self.gui.live_css.clear() changed = remove_names_from_toc(current_container(), spine_names + list(other_items)) if changed: self.gui.toc_view.update_if_visible() for toc in changed: if toc and toc in editors: editors[toc].replace_data(c.raw_data(toc)) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) self.gui.message_popup(ngettext( 'One file deleted', '{} files deleted', len(items)).format(len(items))) def commit_dirty_opf(self): c = current_container() if c.opf_name in editors and not editors[c.opf_name].is_synced_to_container: self.commit_editor_to_container(c.opf_name) self.gui.update_window_title() def reorder_spine(self, items): if not self.ensure_book(): return self.add_savepoint(_('Before: Re-order text')) c = current_container() c.set_spine(items) self.set_modified() self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) completion_worker().clear_caches('names') def add_file(self): if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')): return self.commit_dirty_opf() d = NewFileDialog(self.gui) if d.exec() != QDialog.DialogCode.Accepted: return added_name = self.do_add_file(d.file_name, d.file_data, using_template=d.using_template, edit_file=True) if d.file_name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'): from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_file show_font_face_rule_for_font_file(d.file_data, added_name, self.gui) def do_add_file(self, file_name, data, using_template=False, edit_file=False): self.add_savepoint(_('Before: Add file %s') % self.gui.elided_text(file_name)) c = current_container() adata = data.replace(b'%CURSOR%', b'') if using_template else data spine_index = c.index_in_spine(self.currently_editing or '') if spine_index is not None: spine_index += 1 try: added_name = c.add_file(file_name, adata, spine_index=spine_index) except: self.rewind_savepoint() raise self.gui.file_list.build(c) self.gui.file_list.select_name(file_name) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) mt = c.mime_map[file_name] syntax = syntax_from_mime(file_name, mt) if syntax and edit_file: if using_template: self.edit_file(file_name, syntax, use_template=data.decode('utf-8')) else: self.edit_file(file_name, syntax) self.set_modified() completion_worker().clear_caches('names') return added_name def add_files(self): if not self.ensure_book(_('You must first open a book to edit, before trying to create new files in it.')): return files = choose_files(self.gui, 'tweak-book-bulk-import-files', _('Choose files')) if files: folder_map = get_recommended_folders(current_container(), files) files = {x:('/'.join((folder, os.path.basename(x))) if folder else os.path.basename(x)) for x, folder in iteritems(folder_map)} self.add_savepoint(_('Before Add files')) c = current_container() added_fonts = set() for path in sorted(files, key=numeric_sort_key): name = files[path] i = 0 while c.exists(name) or c.manifest_has_name(name) or c.has_name_case_insensitive(name): i += 1 name, ext = name.rpartition('.')[0::2] name = '%s_%d.%s' % (name, i, ext) try: with open(path, 'rb') as f: c.add_file(name, f.read()) except: self.rewind_savepoint() raise if name.rpartition('.')[2].lower() in ('ttf', 'otf', 'woff'): added_fonts.add(name) self.gui.file_list.build(c) if c.opf_name in editors: editors[c.opf_name].replace_data(c.raw_data(c.opf_name)) self.set_modified() completion_worker().clear_caches('names') if added_fonts: from calibre.gui2.tweak_book.manage_fonts import show_font_face_rule_for_font_files show_font_face_rule_for_font_files(c, added_fonts, self.gui) def add_cover(self): if not self.ensure_book(): return d = AddCover(current_container(), self.gui) d.import_requested.connect(self.do_add_file) try: if d.exec() == QDialog.DialogCode.Accepted and d.file_name is not None: report = [] with BusyCursor(): self.add_savepoint(_('Before: Add cover')) set_cover(current_container(), d.file_name, report.append, options={ 'existing_image':True, 'keep_aspect':tprefs['add_cover_preserve_aspect_ratio']}) self.apply_container_update_to_gui() finally: d.import_requested.disconnect() def ensure_book(self, msg=None): msg = msg or _('No book is currently open. You must first open a book.') if current_container() is None: error_dialog(self.gui, _('No book open'), msg, show=True) return False return True def edit_toc(self): if not self.ensure_book(_('You must open a book before trying to edit the Table of Contents.')): return self.add_savepoint(_('Before: Edit Table of Contents')) d = TOCEditor(title=self.current_metadata.title, parent=self.gui) if d.exec() != QDialog.DialogCode.Accepted: self.rewind_savepoint() return with BusyCursor(): self.set_modified() self.update_editors_from_container() self.gui.toc_view.update_if_visible() self.gui.file_list.build(current_container()) def insert_inline_toc(self): if not self.ensure_book(): return self.commit_all_editors_to_container() self.add_savepoint(_('Before: Insert inline Table of Contents')) name = create_inline_toc(current_container()) if name is None: self.rewind_savepoint() return error_dialog(self.gui, _('No Table of Contents'), _( 'Cannot create an inline Table of Contents as this book has no existing' ' Table of Contents. You must first create a Table of Contents using the' ' Edit Table of Contents tool.'), show=True) self.apply_container_update_to_gui() self.edit_file(name, 'html') def polish(self, action, name, parent=None): if not self.ensure_book(): return from calibre.gui2.tweak_book.polish import get_customization, show_report customization = get_customization(action, name, parent or self.gui) if customization is None: return with BusyCursor(): self.add_savepoint(_('Before: %s') % name) try: report, changed = tweak_polish(current_container(), {action:True}, customization=customization) except: self.rewind_savepoint() raise if changed: self.apply_container_update_to_gui() self.gui.update_window_title() if not changed: self.rewind_savepoint() show_report(changed, self.current_metadata.title, report, parent or self.gui, self.show_current_diff) def transform_html(self): global last_used_html_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')): return from calibre.ebooks.html_transform_rules import transform_container from calibre.gui2.html_transform_rules import RulesDialog d = RulesDialog(self.gui) d.rules = last_used_html_transform_rules d.transform_scope = tprefs['html_transform_scope'] ret = d.exec() last_used_html_transform_rules = d.rules scope = d.transform_scope tprefs.set('html_transform_scope', scope) if ret != QDialog.DialogCode.Accepted: return mime_map = current_container().mime_map names = () if scope == 'current': if not self.currently_editing or mime_map.get(self.currently_editing) not in OEB_DOCS: return error_dialog(self.gui, _('No HTML file'), _('Not currently editing an HTML file'), show=True) names = (self.currently_editing,) elif scope == 'open': names = tuple(name for name in editors if mime_map.get(name) in OEB_DOCS) if not names: return error_dialog(self.gui, _('No HTML files'), _('Not currently editing any HTML files'), show=True) elif scope == 'selected': names = tuple(name for name in self.gui.file_list.file_list.selected_names if mime_map.get(name) in OEB_DOCS) if not names: return error_dialog(self.gui, _('No HTML files'), _('No HTML files are currently selected in the File browser'), show=True) with BusyCursor(): self.add_savepoint(_('Before HTML transformation')) try: changed = transform_container(current_container(), last_used_html_transform_rules, names) except: self.rewind_savepoint() raise if changed: self.apply_container_update_to_gui() if not changed: self.rewind_savepoint() return info_dialog(self.gui, _('No changes'), _('No HTML was changed.'), show=True) self.show_current_diff() def transform_styles(self): global last_used_transform_rules if not self.ensure_book(_('You must first open a book in order to transform styles.')): return from calibre.ebooks.css_transform_rules import transform_container from calibre.gui2.css_transform_rules import RulesDialog d = RulesDialog(self.gui) d.rules = last_used_transform_rules ret = d.exec() last_used_transform_rules = d.rules if ret != QDialog.DialogCode.Accepted: return with BusyCursor(): self.add_savepoint(_('Before style transformation')) try: changed = transform_container(current_container(), last_used_transform_rules) except: self.rewind_savepoint() raise if changed: self.apply_container_update_to_gui() if not changed: self.rewind_savepoint() info_dialog(self.gui, _('No changes'), _( 'No styles were changed.'), show=True) return self.show_current_diff() def get_external_resources(self): if not self.ensure_book(_('You must first open a book in order to transform styles.')): return from calibre.gui2.tweak_book.download import DownloadResources with BusyCursor(): self.add_savepoint(_('Before: Get external resources')) try: d = DownloadResources(self.gui) d.exec() except Exception: self.rewind_savepoint() raise if d.resources_replaced: self.apply_container_update_to_gui() if d.show_diff: self.show_current_diff() else: self.rewind_savepoint() def manage_fonts(self): if not self.ensure_book(_('No book is currently open. You must first open a book to manage fonts.')): return self.commit_all_editors_to_container() self.gui.manage_fonts.display() def manage_fonts_embed(self): self.polish('embed', _('Embed all fonts'), parent=self.gui.manage_fonts) self.gui.manage_fonts.refresh() def manage_fonts_subset(self): self.polish('subset', _('Subset all fonts'), parent=self.gui.manage_fonts) # Renaming {{{ def rationalize_folders(self): if not self.ensure_book(): return c = current_container() if not c.SUPPORTS_FILENAMES: return error_dialog(self.gui, _('Not supported'), _('The %s format does not support file and folder names internally, therefore' ' arranging files into folders is not allowed.') % c.book_type.upper(), show=True) d = RationalizeFolders(self.gui) if d.exec() != QDialog.DialogCode.Accepted: return self.commit_all_editors_to_container() name_map = rationalize_folders(c, d.folder_map) if not name_map: confirm(_( 'The files in this book are already arranged into folders'), 'already-arranged-into-folders', self.gui, pixmap='dialog_information.png', title=_('Nothing to do'), show_cancel_button=False, config_set=tprefs, confirm_msg=_('Show this message &again')) return self.add_savepoint(_('Before: Arrange into folders')) self.gui.blocking_job( 'rationalize_folders', _('Renaming and updating links...'), partial(self.rename_done, name_map), rename_files, current_container(), name_map) def rename_requested(self, oldname, newname): self.commit_all_editors_to_container() if guess_type(oldname) != guess_type(newname): args = os.path.splitext(oldname) + os.path.splitext(newname) if not confirm( _('You are changing the file type of {0}<b>{1}</b> to {2}<b>{3}</b>.' ' Doing so can cause problems, are you sure?').format(*args), 'confirm-filetype-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs): return if urlnormalize(newname) != newname: if not confirm( _('The name you have chosen {0} contains special characters, internally' ' it will look like: {1}Try to use only the English alphabet [a-z], numbers [0-9],' ' hyphens and underscores for file names. Other characters can cause problems for ' ' different e-book viewers. Are you sure you want to proceed?').format( '<pre>%s</pre>'%newname, '<pre>%s</pre>' % urlnormalize(newname)), 'confirm-urlunsafe-change', parent=self.gui, title=_('Are you sure?'), config_set=tprefs): return self.add_savepoint(_('Before: Rename %s') % oldname) name_map = {oldname:newname} self.gui.blocking_job( 'rename_file', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map) def bulk_rename_requested(self, name_map): self.add_savepoint(_('Before: Bulk rename')) self.gui.blocking_job( 'bulk_rename_files', _('Renaming and updating links...'), partial(self.rename_done, name_map, from_filelist=self.gui.file_list.current_name), rename_files, current_container(), name_map) def rename_done(self, name_map, job, from_filelist=None): if job.traceback is not None: self.gui.file_list.restore_temp_names() return error_dialog(self.gui, _('Failed to rename files'), _('Failed to rename files, click "Show details" for more information.'), det_msg=job.traceback, show=True) self.gui.file_list.build(current_container()) self.set_modified() for oldname, newname in iteritems(name_map): if oldname in editors: editors[newname] = ed = editors.pop(oldname) ed.change_document_name(newname) self.gui.central.rename_editor(editors[newname], newname) if self.gui.preview.current_name == oldname: self.gui.preview.current_name = newname self.apply_container_update_to_gui() if from_filelist: self.gui.file_list.select_names(frozenset(itervalues(name_map)), current_name=name_map.get(from_filelist)) self.gui.file_list.file_list.setFocus(Qt.FocusReason.PopupFocusReason) # }}} # Global history {{{ def do_global_undo(self, *a): container = self.global_undo.undo() if container is not None: set_current_container(container) self.apply_container_update_to_gui() def do_global_redo(self): container = self.global_undo.redo() if container is not None: set_current_container(container) self.apply_container_update_to_gui() def update_global_history_actions(self): gu = self.global_undo for x, text in (('undo', _('&Revert to')), ('redo', _('&Revert to'))): ac = getattr(self.gui, 'action_global_%s' % x) ac.setEnabled(getattr(gu, 'can_' + x)) ac.setText(text + ' "%s"'%(getattr(gu, x + '_msg') or '...'))

[documentos] def add_savepoint(self, msg): ' Create a restore checkpoint with the name specified as ``msg`` ' self.commit_all_editors_to_container() nc = clone_container(current_container(), self.mkdtemp()) self.global_undo.add_savepoint(nc, msg) set_current_container(nc) self.update_global_history_actions()

[documentos] def rewind_savepoint(self): ' Undo the previous creation of a restore checkpoint, useful if you create a checkpoint, then abort the operation with no changes ' container = self.global_undo.rewind_savepoint() if container is not None: set_current_container(container) self.update_global_history_actions()

def create_diff_dialog(self, revert_msg=_('&Revert changes'), show_open_in_editor=True): global _diff_dialogs from calibre.gui2.tweak_book.diff.main import Diff def line_activated(name, lnum, right): if right: self.edit_file_requested(name, None, guess_type(name)) if name in editors: editor = editors[name] editor.go_to_line(lnum) editor.setFocus(Qt.FocusReason.OtherFocusReason) self.gui.raise_and_focus() d = Diff(revert_button_msg=revert_msg, show_open_in_editor=show_open_in_editor) [x.break_cycles() for x in _diff_dialogs if not x.isVisible()] _diff_dialogs = [x for x in _diff_dialogs if x.isVisible()] + [d] d.show(), d.raise_and_focus(), d.setFocus(Qt.FocusReason.OtherFocusReason), d.setWindowModality(Qt.WindowModality.NonModal) if show_open_in_editor: d.line_activated.connect(line_activated) return d

[documentos] def show_current_diff(self, allow_revert=True, to_container=None): ''' Show the changes to the book from its last checkpointed state :param allow_revert: If True the diff dialog will have a button to allow the user to revert all changes :param to_container: A container object to compare the current container to. If None, the previously checkpointed container is used ''' self.commit_all_editors_to_container() k = {} if allow_revert else {'revert_msg': None} d = self.create_diff_dialog(**k) previous_container = self.global_undo.previous_container connect_lambda(d.revert_requested, self, lambda self: self.revert_requested(previous_container)) other = to_container or self.global_undo.previous_container d.container_diff(other, self.global_undo.current_container, names=(self.global_undo.label_for_container(other), self.global_undo.label_for_container(self.global_undo.current_container)))

def ask_to_show_current_diff(self, name, title, msg, allow_revert=True, to_container=None): if tprefs.get('skip_ask_to_show_current_diff_for_' + name): return d = QDialog(self.gui) k = QVBoxLayout(d) d.setWindowTitle(title) k.addWidget(QLabel(msg)) k.confirm = cb = QCheckBox(_('Show this popup again')) k.addWidget(cb) cb.setChecked(True) connect_lambda(cb.toggled, d, lambda d, checked: tprefs.set('skip_ask_to_show_current_diff_for_' + name, not checked)) d.bb = bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, d) k.addWidget(bb) bb.accepted.connect(d.accept) bb.rejected.connect(d.reject) d.b = b = bb.addButton(_('See what &changed'), QDialogButtonBox.ButtonRole.AcceptRole) b.setIcon(QIcon.ic('diff.png')), b.setAutoDefault(False) bb.button(QDialogButtonBox.StandardButton.Close).setDefault(True) if d.exec() == QDialog.DialogCode.Accepted: self.show_current_diff(allow_revert=allow_revert, to_container=to_container) def compare_book(self): if not self.ensure_book(): return self.commit_all_editors_to_container() c = current_container() path = choose_files(self.gui, 'select-book-for-comparison', _('Choose book'), filters=[ (_('%s books') % c.book_type.upper(), (c.book_type,))], select_only_single_file=True, all_files=False) if path and path[0]: with TemporaryDirectory('_compare') as tdir: other = _gc(path[0], tdir=tdir, tweak_mode=True) d = self.create_diff_dialog(revert_msg=None) d.container_diff(other, c, names=(_('Other book'), _('Current book'))) def revert_requested(self, container): self.commit_all_editors_to_container() nc = self.global_undo.revert_to(container) set_current_container(nc) self.apply_container_update_to_gui() def compare_requested(self, container): self.show_current_diff(to_container=container) # }}}

[documentos] def set_modified(self): ' Mark the book as having been modified ' self.gui.action_save.setEnabled(True)

def request_completion(self, name, completion_type, completion_data, query=None): if completion_type is None: completion_worker().clear_caches(completion_data) return request_id = (self.completion_request_count, name) self.completion_request_count += 1 completion_worker().queue_completion(request_id, completion_type, completion_data, query) return request_id[0] def handle_completion_result(self, result): name = result.request_id[1] editor = editors.get(name) if editor is not None: editor.handle_completion_result(result) def fix_html(self, current): if current: ed = self.gui.central.current_editor if hasattr(ed, 'fix_html'): ed.fix_html() else: with BusyCursor(): self.add_savepoint(_('Before: Fix HTML')) fix_all_html(current_container()) self.update_editors_from_container() self.set_modified() self.ask_to_show_current_diff('html-fix', _('Fixing done'), _('All HTML files fixed')) def pretty_print(self, current): if current: ed = self.gui.central.current_editor ed.pretty_print(editor_name(ed)) else: with BusyCursor(): self.add_savepoint(_('Before: Beautify files')) pretty_all(current_container()) self.update_editors_from_container() self.set_modified() QApplication.alert(self.gui) self.ask_to_show_current_diff('beautify', _('Beautified'), _('All files beautified')) def mark_selected_text(self): ed = self.gui.central.current_editor if ed is not None: ed.mark_selected_text() if ed.has_marked_text: self.gui.central.search_panel.set_where('selected-text') else: self.gui.central.search_panel.unset_marked() def editor_action(self, action): ed = self.gui.central.current_editor edname = editor_name(ed) if hasattr(ed, 'action_triggered'): if action and action[0] == 'insert_resource': rtype = action[1] if rtype == 'image' and ed.syntax not in {'css', 'html'}: return error_dialog(self.gui, _('Not supported'), _( 'Inserting images is only supported for HTML and CSS files.'), show=True) rdata = get_resource_data(rtype, self.gui) if rdata is None: return if rtype == 'image': chosen_name, chosen_image_is_external, fullpage, preserve_ar = rdata if chosen_image_is_external: with open(chosen_image_is_external[1], 'rb') as f: current_container().add_file(chosen_image_is_external[0], f.read()) self.refresh_file_list() chosen_name = chosen_image_is_external[0] href = current_container().name_to_href(chosen_name, edname) fmt, width, height = identify(current_container().raw_data(chosen_name, decode=False)) ed.insert_image(href, fullpage=fullpage, preserve_aspect_ratio=preserve_ar, width=width, height=height) elif action[0] == 'insert_hyperlink': self.commit_all_editors_to_container() d = InsertLink(current_container(), edname, initial_text=ed.get_smart_selection(), parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted: ed.insert_hyperlink(d.href, d.text, template=d.rendered_template) elif action[0] == 'insert_tag': d = InsertTag(parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted: ed.insert_tag(d.tag) else: ed.action_triggered(action) def rename_class(self, class_name): self.commit_all_editors_to_container() text, ok = QInputDialog.getText(self.gui, _('New class name'), _( 'Rename the class {} to?').format(class_name)) if ok: self.add_savepoint(_('Before: Rename {}').format(class_name)) with BusyCursor(): changed = rename_class(current_container(), class_name, text.strip()) if changed: self.apply_container_update_to_gui() self.show_current_diff() else: self.rewind_savepoint() return info_dialog(self.gui, _('No matches'), _( 'No class {} found to change').format(class_name), show=True) def set_semantics(self): if not self.ensure_book(): return self.commit_all_editors_to_container() c = current_container() if c.book_type == 'azw3': return error_dialog(self.gui, _('Not supported'), _( 'Semantics are not supported for the AZW3 format.'), show=True) d = InsertSemantics(c, parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted and d.changes: self.add_savepoint(_('Before: Set Semantics')) d.apply_changes(current_container()) self.apply_container_update_to_gui() def filter_css(self): self.commit_all_editors_to_container() c = current_container() ed = self.gui.central.current_editor current_name = editor_name(ed) if current_name and c.mime_map[current_name] not in OEB_DOCS | OEB_STYLES: current_name = None d = FilterCSS(current_name=current_name, parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted and d.filtered_properties: self.add_savepoint(_('Before: Filter style information')) with BusyCursor(): changed = filter_css(current_container(), d.filtered_properties, names=d.filter_names) if changed: self.apply_container_update_to_gui() self.show_current_diff() else: self.rewind_savepoint() return info_dialog(self.gui, _('No matches'), _( 'No matching style rules were found'), show=True) def show_find(self): self.gui.central.show_find() ed = self.gui.central.current_editor if ed is not None and hasattr(ed, 'selected_text'): text = ed.selected_text if text and text.strip(): self.gui.central.pre_fill_search(text) def show_text_search(self): self.gui.text_search_dock.show() self.gui.text_search.find.setFocus(Qt.FocusReason.OtherFocusReason) def search_action_triggered(self, action, overrides=None): ss = self.gui.saved_searches.isVisible() trigger_saved_search = ss and (not self.gui.central.search_panel.isVisible() or self.gui.saved_searches.has_focus()) if trigger_saved_search: return self.gui.saved_searches.trigger_action(action, overrides=overrides) self.search(action, overrides) def run_saved_searches(self, searches, action): ed = self.gui.central.current_editor name = editor_name(ed) searchable_names = self.gui.file_list.searchable_names if not searches or not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), searches[0], self.gui): return ret = run_search(searches, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified) ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False): ed.editor.setFocus(Qt.FocusReason.OtherFocusReason) else: self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason) def search(self, action, overrides=None): # Run a search/replace sp = self.gui.central.search_panel # Ensure the search panel is visible sp.setVisible(True) ed = self.gui.central.current_editor name = editor_name(ed) state = sp.state if overrides: state.update(overrides) searchable_names = self.gui.file_list.searchable_names if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui): return ret = run_search(state, action, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file, self.show_current_diff, self.add_savepoint, self.rewind_savepoint, self.set_modified) ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False): ed.editor.setFocus(Qt.FocusReason.OtherFocusReason) else: self.gui.saved_searches.setFocus(Qt.FocusReason.OtherFocusReason) def find_text(self, state): from calibre.gui2.tweak_book.text_search import run_text_search searchable_names = self.gui.file_list.searchable_names ed = self.gui.central.current_editor name = editor_name(ed) if not validate_search_request(name, searchable_names, getattr(ed, 'has_marked_text', False), state, self.gui): return ret = run_text_search(state, ed, name, searchable_names, self.gui, self.show_editor, self.edit_file) ed = ret is True and self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False): ed.editor.setFocus(Qt.FocusReason.OtherFocusReason) def find_initial_text(self, text): from calibre.gui2.tweak_book.search import get_search_regex from calibre.gui2.tweak_book.text_search import file_matches_pattern search = {'find': text, 'mode': 'normal', 'case_sensitive': True, 'direction': 'down'} pat = get_search_regex(search) searchable_names = set(self.gui.file_list.searchable_names['text']) for name, ed in iteritems(editors): searchable_names.discard(name) if ed.find_text(pat, complete=True): self.show_editor(name) return for name in searchable_names: if file_matches_pattern(name, pat): self.edit_file(name) if editors[name].find_text(pat, complete=True): return def find_word(self, word, locations): # Go to a word from the spell check dialog ed = self.gui.central.current_editor name = editor_name(ed) find_next_word(word, locations, ed, name, self.gui, self.show_editor, self.edit_file) def next_spell_error(self): # Go to the next spelling error ed = self.gui.central.current_editor name = editor_name(ed) find_next_error(ed, name, self.gui, self.show_editor, self.edit_file, self.close_editor) def word_change_requested(self, w, new_word): if self.commit_all_editors_to_container(): self.gui.spell_check.change_word_after_update(w, new_word) else: self.gui.spell_check.do_change_word(w, new_word) def word_replaced(self, changed_names): self.set_modified() self.update_editors_from_container(names=set(changed_names)) def word_ignored(self, word, locale): if tprefs['inline_spell_check']: for ed in itervalues(editors): try: ed.editor.recheck_word(word, locale) except AttributeError: pass def editor_link_clicked(self, url): ed = self.gui.central.current_editor name = editor_name(ed) if url.startswith('#'): target = name else: target = current_container().href_to_name(url, name) frag = url.partition('#')[-1] if current_container().has_name(target): self.link_clicked(target, frag, show_anchor_not_found=True) else: try: purl = urlparse(url) except ValueError: return if purl.scheme not in {'', 'file'}: open_url(QUrl(url)) else: error_dialog(self.gui, _('Not found'), _( 'No file with the name %s was found in the book') % target, show=True) def editor_class_clicked(self, class_data): from calibre.gui2.tweak_book.jump_to_class import NoMatchingRuleFound, NoMatchingTagFound, find_first_matching_rule ed = self.gui.central.current_editor name = editor_name(ed) try: res = find_first_matching_rule(current_container(), name, ed.get_raw_data(), class_data) except (NoMatchingTagFound, NoMatchingRuleFound): res = None if res is not None and res.file_name and res.rule_address: editor = self.open_editor_for_name(res.file_name) if editor: editor.goto_css_rule(res.rule_address, sourceline_address=res.style_tag_address) else: error_dialog(self.gui, _('No matches found'), _('No style rules that match the class {} were found').format( class_data['class']), show=True) def save_search(self): state = self.gui.central.search_panel.state self.show_saved_searches() self.gui.saved_searches.add_predefined_search(state) def show_saved_searches(self): self.gui.saved_searches_dock.show() saved_searches = show_saved_searches def create_checkpoint(self): text, ok = QInputDialog.getText(self.gui, _('Choose name'), _( 'Choose a name for the checkpoint.\nYou can later restore the book' ' to this checkpoint via the\n"Revert to..." entries in the Edit menu.')) if ok: self.add_savepoint(text) def commit_editor_to_container(self, name, container=None): container = container or current_container() ed = editors[name] with container.open(name, 'wb') as f: f.write(ed.data) if name == container.opf_name: container.refresh_mime_map() lang = container.opf_xpath('//dc:language/text()') or [self.current_metadata.language] set_book_locale(lang[0]) if container is current_container(): ed.is_synced_to_container = True if name == container.opf_name: self.gui.file_list.build(container)

[documentos] def commit_all_editors_to_container(self): ''' Commit any changes that the user has made to files open in editors to the container. You should call this method before performing any actions on the current container ''' changed = False with BusyCursor(): for name, ed in iteritems(editors): if not ed.is_synced_to_container: self.commit_editor_to_container(name) ed.is_synced_to_container = True changed = True return changed

[documentos] def save_book(self): ' Save the book. Saving is performed in the background ' self.gui.update_window_title() c = current_container() for name, ed in iteritems(editors): if ed.is_modified or not ed.is_synced_to_container: self.commit_editor_to_container(name, c) ed.is_modified = False path_to_ebook = os.path.abspath(c.path_to_ebook) destdir = os.path.dirname(path_to_ebook) if not c.is_dir and not os.path.exists(destdir): info_dialog(self.gui, _('Path does not exist'), _( 'The file you are editing (%s) no longer exists. You have to choose a new save location.') % path_to_ebook, show_copy_button=False, show=True) fmt = path_to_ebook.rpartition('.')[-1].lower() start_dir = find_first_existing_ancestor(path_to_ebook) path = choose_save_file( self.gui, 'choose-new-save-location', _('Choose file location'), initial_path=os.path.join(start_dir, os.path.basename(path_to_ebook)), filters=[(fmt.upper(), (fmt,))], all_files=False) if path is not None: if not path.lower().endswith('.' + fmt): path = path + '.' + fmt path = os.path.abspath(path) c.path_to_ebook = path self.global_undo.update_path_to_ebook(path) else: return if os.path.exists(c.path_to_ebook) and not os.access(c.path_to_ebook, os.W_OK): if not question_dialog(self.gui, _('File is read-only'), _( 'The file at {} is read-only. The editor will try to reset its permissions before saving. Proceed with saving?' ).format(c.path_to_ebook), override_icon='dialog_warning.png', yes_text=_('&Save'), no_text=_('&Cancel'), yes_icon='save.png'): return self.gui.action_save.setEnabled(False) tdir = self.mkdtemp(prefix='save-') container = clone_container(c, tdir) self.save_manager.schedule(tdir, container)

def _save_copy(self, post_action=None): self.gui.update_window_title() c = current_container() if c.is_dir: return error_dialog(self.gui, _('Cannot save a copy'), _( 'Saving a copy of a folder based book is not supported'), show=True) ext = c.path_to_ebook.rpartition('.')[-1] path = choose_save_file( self.gui, 'tweak_book_save_copy', _('Choose path'), initial_filename=self.current_metadata.title + '.' + ext, filters=[(_('Book (%s)') % ext.upper(), [ext.lower()])], all_files=False) if not path: return if '.' not in os.path.basename(path): path += '.' + ext.lower() tdir = self.mkdtemp(prefix='save-copy-') container = clone_container(c, tdir) for name, ed in iteritems(editors): if ed.is_modified or not ed.is_synced_to_container: self.commit_editor_to_container(name, container) def do_save(c, path, tdir): save_container(c, path) shutil.rmtree(tdir, ignore_errors=True) return path self.gui.blocking_job('save_copy', _('Saving copy, please wait...'), partial(self.copy_saved, post_action=post_action), do_save, container, path, tdir) def save_copy(self): self._save_copy() def copy_saved(self, job, post_action=None): if job.traceback is not None: return error_dialog(self.gui, _('Failed to save copy'), _('Failed to save copy, click "Show details" for more information.'), det_msg=job.traceback, show=True) msg = _('Copy saved to %s') % job.result if post_action is None: info_dialog(self.gui, _('Copy saved'), msg, show=True) elif post_action == 'replace': msg = _('Editing saved copy at: %s') % job.result self.open_book(job.result, edit_file=self.currently_editing) elif post_action == 'edit': if ismacos: cmd = ['open', '-F', '-n', '-a', os.path.dirname(os.path.dirname(os.path.dirname(macos_edit_book_bundle_path()))), '--args'] else: cmd = [exe_path('ebook-edit')] if islinux: cmd.append('--detach') cmd.append(job.result) ce = self.currently_editing if ce: cmd.append(ce) with sanitize_env_vars(): subprocess.Popen(cmd) self.gui.show_status_message(msg, 5) def report_save_error(self, tb): if self.doing_terminal_save: prints(tb, file=sys.stderr) self.abort_terminal_save() self.set_modified() error_dialog(self.gui, _('Could not save'), _('Saving of the book failed. Click "Show details"' ' for more information. You can try to save a copy' ' to a different location, via File->Save a copy'), det_msg=tb, show=True) def go_to_line_number(self): ed = self.gui.central.current_editor if ed is None or not ed.has_line_numbers: return num, ok = QInputDialog.getInt(self.gui, _('Enter line number'), ('Line number:'), ed.current_line, 1, max(100000, ed.number_of_lines)) if ok: ed.current_line = num def split_start_requested(self): self.commit_all_editors_to_container() self.gui.preview.do_start_split() @in_thread_job def split_requested(self, name, loc, totals): self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name)) try: bottom_name = split(current_container(), name, loc, totals=totals) except AbortError: self.rewind_savepoint() raise self.apply_container_update_to_gui() self.edit_file(bottom_name, 'html') def multisplit(self): ed = self.gui.central.current_editor if ed.syntax != 'html': return name = editor_name(ed) if name is None: return d = MultiSplit(self.gui) if d.exec() == QDialog.DialogCode.Accepted: with BusyCursor(): self.add_savepoint(_('Before: Split %s') % self.gui.elided_text(name)) try: multisplit(current_container(), name, d.xpath) except AbortError: self.rewind_savepoint() raise self.apply_container_update_to_gui() def open_editor_for_name(self, name): if name in editors: editor = editors[name] self.gui.central.show_editor(editor) else: try: mt = current_container().mime_map[name] except KeyError: error_dialog(self.gui, _('Does not exist'), _( 'The file %s does not exist. If you were trying to click an item in' ' the Table of Contents, you may' ' need to refresh it by right-clicking and choosing "Refresh".') % name, show=True) return None syntax = syntax_from_mime(name, mt) if not syntax: error_dialog( self.gui, _('Unsupported file format'), _('Editing files of type %s is not supported') % mt, show=True) return None editor = self.edit_file(name, syntax) return editor def link_clicked(self, name, anchor, show_anchor_not_found=False): if not name: return editor = self.open_editor_for_name(name) if anchor and hasattr(editor, 'go_to_anchor') : if editor.go_to_anchor(anchor): self.gui.preview.pending_go_to_anchor = anchor elif show_anchor_not_found: error_dialog(self.gui, _('Not found'), _( 'The anchor %s was not found in this file') % anchor, show=True) @in_thread_job def check_item_activated(self, item): is_mult = item.has_multiple_locations and getattr(item, 'current_location_index', None) is not None name = item.all_locations[item.current_location_index][0] if is_mult else item.name editor = None if name in editors: editor = editors[name] self.gui.central.show_editor(editor) else: try: editor = self.edit_file_requested(name, None, current_container().mime_map[name]) except KeyError: error_dialog(self.gui, _('File deleted'), _( 'The file {} has already been deleted, re-run Check Book to update the results.').format(name), show=True) if getattr(editor, 'has_line_numbers', False): if is_mult: editor.go_to_line(*(item.all_locations[item.current_location_index][1:3])) else: editor.go_to_line(item.line or 0, item.col or 0) editor.set_focus() @in_thread_job def check_requested(self, *args): if current_container() is None: return self.commit_all_editors_to_container() c = self.gui.check_book c.parent().show() c.parent().raise_and_focus() c.run_checks(current_container()) def spell_check_requested(self): if current_container() is None: return self.commit_all_editors_to_container() self.add_savepoint(_('Before: Spell Check')) self.gui.spell_check.show() @in_thread_job def fix_requested(self, errors): self.add_savepoint(_('Before: Auto-fix errors')) c = self.gui.check_book c.parent().show() c.parent().raise_and_focus() changed = c.fix_errors(current_container(), errors) if changed: self.apply_container_update_to_gui() self.set_modified() else: self.rewind_savepoint() @in_thread_job def merge_requested(self, category, names, master): self.add_savepoint(_('Before: Merge files into %s') % self.gui.elided_text(master)) try: merge(current_container(), category, names, master) except AbortError: self.rewind_savepoint() raise self.apply_container_update_to_gui() if master in editors: self.show_editor(master) self.gui.file_list.merge_completed(master) self.gui.message_popup(_('{} files merged').format(len(names))) @in_thread_job def link_stylesheets_requested(self, names, sheets, remove): self.add_savepoint(_('Before: Link stylesheets')) changed_names = link_stylesheets(current_container(), names, sheets, remove) if changed_names: self.update_editors_from_container(names=changed_names) self.set_modified() @in_thread_job def export_requested(self, name_or_names, path): if isinstance(name_or_names, string_or_bytes): return self.export_file(name_or_names, path) for name in name_or_names: dest = os.path.abspath(os.path.join(path, name)) if '/' in name or os.sep in name: try: os.makedirs(os.path.dirname(dest)) except OSError as err: if err.errno != errno.EEXIST: raise self.export_file(name, dest) def open_file_with(self, file_name, fmt, entry): if file_name in editors and not editors[file_name].is_synced_to_container: self.commit_editor_to_container(file_name) with current_container().open(file_name) as src: tdir = PersistentTemporaryDirectory(suffix='-ee-ow') with open(os.path.join(tdir, os.path.basename(file_name)), 'wb') as dest: shutil.copyfileobj(src, dest) from calibre.gui2.open_with import run_program run_program(entry, dest.name, self) if question_dialog(self.gui, _('File opened'), _( 'When you are done editing {0} click "Import" to update' ' the file in the book or "Discard" to lose any changes.').format(file_name), yes_text=_('Import'), no_text=_('Discard') ): self.add_savepoint(_('Before: Replace %s') % file_name) with open(dest.name, 'rb') as src, current_container().open(file_name, 'wb') as cdest: shutil.copyfileobj(src, cdest) self.apply_container_update_to_gui() try: shutil.rmtree(tdir) except Exception: pass @in_thread_job def copy_files_to_clipboard(self, names): names = tuple(names) for name in names: if name in editors and not editors[name].is_synced_to_container: self.commit_editor_to_container(name) container = current_container() md = QMimeData() url_map = { name:container.get_file_path_for_processing(name, allow_modification=False) for name in names } md.setUrls(list(map(QUrl.fromLocalFile, list(url_map.values())))) import json md.setData(FILE_COPY_MIME, as_bytes(json.dumps({ name: (url_map[name], container.mime_map.get(name)) for name in names }))) QApplication.instance().clipboard().setMimeData(md) @in_thread_job def paste_files_from_clipboard(self): md = QApplication.instance().clipboard().mimeData() if md.hasUrls() and md.hasFormat(FILE_COPY_MIME): import json self.commit_all_editors_to_container() name_map = json.loads(bytes(md.data(FILE_COPY_MIME))) container = current_container() for name, (path, mt) in iteritems(name_map): with open(path, 'rb') as f: container.add_file(name, f.read(), media_type=mt, modify_name_if_needed=True) self.apply_container_update_to_gui() def export_file(self, name, path): if name in editors and not editors[name].is_synced_to_container: self.commit_editor_to_container(name) with current_container().open(name, 'rb') as src, open(path, 'wb') as dest: shutil.copyfileobj(src, dest) @in_thread_job def replace_requested(self, name, path, basename, force_mt): self.add_savepoint(_('Before: Replace %s') % name) replace_file(current_container(), name, path, basename, force_mt) self.apply_container_update_to_gui() def browse_images(self): self.gui.image_browser.refresh() self.gui.image_browser.show() self.gui.image_browser.raise_and_focus() def show_reports(self): if not self.ensure_book(_('You must first open a book in order to see the report.')): return self.gui.reports.refresh() self.gui.reports.show() self.gui.reports.raise_and_focus() def reports_edit_requested(self, name): mt = current_container().mime_map.get(name, guess_type(name)) self.edit_file_requested(name, None, mt) def image_activated(self, name): mt = current_container().mime_map.get(name, guess_type(name)) self.edit_file_requested(name, None, mt) def check_external_links(self): if self.ensure_book(_('You must first open a book in order to check links.')): self.commit_all_editors_to_container() self.gui.check_external_links.show() def compress_images(self): if not self.ensure_book(_('You must first open a book in order to compress images.')): return from calibre.gui2.tweak_book.polish import CompressImages, CompressImagesProgress, show_report d = CompressImages(self.gui) if d.exec() == QDialog.DialogCode.Accepted: with BusyCursor(): self.add_savepoint(_('Before: compress images')) d = CompressImagesProgress(names=d.names, jpeg_quality=d.jpeg_quality, webp_quality=d.webp_quality, parent=self.gui) if d.exec() != QDialog.DialogCode.Accepted: self.rewind_savepoint() return changed, report = d.result if changed is None and report: self.rewind_savepoint() return error_dialog(self.gui, _('Unexpected error'), _( 'Failed to compress images, click "Show details" for more information'), det_msg=report, show=True) if changed: self.apply_container_update_to_gui() else: self.rewind_savepoint() show_report(changed, self.current_metadata.title, report, self.gui, self.show_current_diff) def sync_editor_to_preview(self, name, sourceline_address): editor = self.edit_file(name, 'html') self.ignore_preview_to_editor_sync = True try: editor.goto_sourceline(*sourceline_address) finally: self.ignore_preview_to_editor_sync = False def do_sync_preview_to_editor(self, wait_for_highlight_to_finish=False): if self.ignore_preview_to_editor_sync: return ed = self.gui.central.current_editor if ed is not None: name = editor_name(ed) if name is not None and getattr(ed, 'syntax', None) == 'html': hl = getattr(ed, 'highlighter', None) if wait_for_highlight_to_finish: if getattr(hl, 'is_working', False): QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish) return ct = ed.current_tag() self.gui.preview.sync_to_editor(name, ct) if hl is not None and hl.is_working: QTimer.singleShot(75, self.sync_preview_to_editor_on_highlight_finish)

[documentos] def sync_preview_to_editor(self): ' Sync the position of the preview panel to the current cursor position in the current editor ' self.do_sync_preview_to_editor()

def sync_preview_to_editor_on_highlight_finish(self): self.do_sync_preview_to_editor(wait_for_highlight_to_finish=True) def show_partial_cfi_in_editor(self, name, cfi): editor = self.edit_file(name, 'html') if not editor or not editor.has_line_numbers: return False from calibre.ebooks.epub.cfi.parse import decode_cfi from calibre.ebooks.oeb.polish.parsing import parse root = parse( editor.get_raw_data(), decoder=lambda x: x.decode('utf-8'), line_numbers=True, linenumber_attribute='data-lnum') node = decode_cfi(root, cfi) def barename(x): return x.tag.partition('}')[-1] if node is not None: lnum = node.get('data-lnum') if lnum: tags_before = [] for tag in root.xpath('//*[@data-lnum="%s"]' % lnum): tags_before.append(barename(tag)) if tag is node: break else: tags_before.append(barename(node)) lnum = int(lnum) return editor.goto_sourceline(lnum, tags_before, attribute='id' if node.get('id') else None) return False def goto_style_declaration(self, data): name = data['name'] editor = self.edit_file(name, syntax=data['syntax']) self.gui.live_css.navigate_to_declaration(data, editor) def init_editor(self, name, editor, data=None, use_template=False): editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed) editor.data_changed.connect(self.editor_data_changed) editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed) editor.cursor_position_changed.connect(self.sync_preview_to_editor) editor.cursor_position_changed.connect(self.update_cursor_position) if hasattr(editor, 'word_ignored'): editor.word_ignored.connect(self.word_ignored) if hasattr(editor, 'link_clicked'): editor.link_clicked.connect(self.editor_link_clicked) if hasattr(editor, 'class_clicked'): editor.class_clicked.connect(self.editor_class_clicked) if hasattr(editor, 'rename_class'): editor.rename_class.connect(self.rename_class) if getattr(editor, 'syntax', None) == 'html': editor.smart_highlighting_updated.connect(self.gui.live_css.sync_to_editor) if hasattr(editor, 'set_request_completion'): editor.set_request_completion(partial(self.request_completion, name), name) if data is not None: if use_template: editor.init_from_template(data) else: editor.data = data editor.is_synced_to_container = True editor.modification_state_changed.connect(self.editor_modification_state_changed) self.gui.central.add_editor(name, editor)

[documentos] def edit_file(self, name, syntax=None, use_template=None): ''' Open the file specified by name in an editor :param syntax: The media type of the file, for example, ``'text/html'``. If not specified it is guessed from the file extension. :param use_template: A template to initialize the opened editor with ''' editor = editors.get(name, None) if editor is None: syntax = syntax or syntax_from_mime(name, guess_type(name)) if use_template is None: data = current_container().raw_data(name) if isbytestring(data) and syntax in {'html', 'css', 'text', 'xml', 'javascript'}: try: data = data.decode('utf-8') except UnicodeDecodeError: return error_dialog(self.gui, _('Cannot decode'), _( 'Cannot edit %s as it appears to be in an unknown character encoding') % name, show=True) else: data = use_template editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs) self.init_editor(name, editor, data, use_template=bool(use_template)) if tprefs['pretty_print_on_open']: editor.pretty_print(name) self.show_editor(name) return editor

[documentos] def show_editor(self, name): ' Show the editor that is editing the file specified by ``name`` ' self.gui.central.show_editor(editors[name]) editors[name].set_focus()

def edit_file_requested(self, name, syntax=None, mime=None): if name in editors: self.gui.central.show_editor(editors[name]) return editors[name] mime = mime or current_container().mime_map.get(name, guess_type(name)) syntax = syntax or syntax_from_mime(name, mime) if not syntax: return error_dialog( self.gui, _('Unsupported file format'), _('Editing files of type %s is not supported') % mime, show=True) return self.edit_file(name, syntax) def edit_next_file(self, backwards=False): self.gui.file_list.edit_next_file(self.currently_editing, backwards) def quick_open(self): if not self.ensure_book(_('No book is currently open. You must first open a book to edit.')): return c = current_container() files = [name for name, mime in iteritems(c.mime_map) if c.exists(name) and syntax_from_mime(name, mime) is not None] d = QuickOpen(files, parent=self.gui) if d.exec() == QDialog.DialogCode.Accepted and d.selected_result is not None: self.edit_file_requested(d.selected_result, None, c.mime_map[d.selected_result]) # Editor basic controls {{{ def do_editor_undo(self): ed = self.gui.central.current_editor if ed is not None: ed.undo() def do_editor_redo(self): ed = self.gui.central.current_editor if ed is not None: ed.redo() def do_editor_copy(self): ed = self.gui.central.current_editor if ed is not None: ed.copy() def do_editor_cut(self): ed = self.gui.central.current_editor if ed is not None: ed.cut() def do_editor_paste(self): ed = self.gui.central.current_editor if ed is not None: ed.paste() def editor_data_changed(self, editor): self.gui.preview.start_refresh_timer() for name, ed in iteritems(editors): if ed is editor: self.gui.toc_view.start_refresh_timer(name) break def editor_undo_redo_state_changed(self, *args): self.apply_current_editor_state() def editor_copy_available_state_changed(self, *args): self.apply_current_editor_state() def editor_modification_state_changed(self, is_modified): self.apply_current_editor_state() if is_modified: self.set_modified() # }}} def apply_current_editor_state(self): ed = self.gui.central.current_editor self.gui.cursor_position_widget.update_position() if ed is not None: actions['editor-undo'].setEnabled(ed.undo_available) actions['editor-redo'].setEnabled(ed.redo_available) actions['editor-copy'].setEnabled(ed.copy_available) actions['editor-cut'].setEnabled(ed.cut_available) actions['go-to-line-number'].setEnabled(ed.has_line_numbers) actions['fix-html-current'].setEnabled(ed.syntax == 'html') name = editor_name(ed) mime = current_container().mime_map.get(name) if name is not None and (getattr(ed, 'syntax', None) == 'html' or mime == 'image/svg+xml'): if self.gui.preview.show(name): # The file being displayed by the preview has changed. # Set the preview's position to the current cursor # position in the editor, in case the editors' cursor # position has not changed, since the last time it was # focused. This is not inefficient since multiple requests # to sync are de-bounced with a 100 msec wait. self.sync_preview_to_editor() if name is not None: self.gui.file_list.mark_name_as_current(name) if ed.has_line_numbers: self.gui.cursor_position_widget.update_position(*ed.cursor_position) else: actions['go-to-line-number'].setEnabled(False) self.gui.file_list.clear_currently_edited_name() def update_cursor_position(self): ed = self.gui.central.current_editor if getattr(ed, 'has_line_numbers', False): self.gui.cursor_position_widget.update_position(*ed.cursor_position) else: self.gui.cursor_position_widget.update_position() def editor_close_requested(self, editor): name = editor_name(editor) if not name: return if not editor.is_synced_to_container: self.commit_editor_to_container(name) self.close_editor(name)

[documentos] def close_editor(self, name): ' Close the editor that is editing the file specified by ``name`` ' editor = editors.pop(name) self.gui.central.close_editor(editor) editor.break_cycles() if not editors or getattr(self.gui.central.current_editor, 'syntax', None) != 'html': self.gui.preview.clear() self.gui.live_css.clear()

def insert_character(self): self.gui.insert_char.show() def manage_snippets(self): from calibre.gui2.tweak_book.editor.snippets import UserSnippets UserSnippets(self.gui).exec() def merge_files(self): self.gui.file_list.merge_files() # Shutdown {{{ def quit(self): if self.doing_terminal_save: return False if self.save_manager.has_tasks: if question_dialog( self.gui, _('Are you sure?'), _( 'The current book is being saved in the background. Quitting now will' ' <b>abort the save process</b>! Finish saving first?'), yes_text=_('Finish &saving first'), no_text=_('&Quit immediately')): if self.save_manager.has_tasks: self.start_terminal_save_indicator() return False if not self.confirm_quit(): return False self.shutdown() QApplication.instance().exit() return True def confirm_quit(self): if self.gui.action_save.isEnabled(): d = QDialog(self.gui) d.l = QGridLayout(d) d.setLayout(d.l) d.setWindowTitle(_('Unsaved changes')) d.i = QLabel('') d.i.setMaximumSize(QSize(64, 64)) d.i.setPixmap(QIcon.ic('dialog_warning.png').pixmap(d.i.maximumSize())) d.l.addWidget(d.i, 0, 0) d.m = QLabel(_('There are unsaved changes, if you quit without saving, you will lose them.')) d.m.setWordWrap(True) d.l.addWidget(d.m, 0, 1) d.bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Cancel) d.bb.rejected.connect(d.reject) d.bb.accepted.connect(d.accept) d.l.addWidget(d.bb, 1, 0, 1, 2) d.do_save = None def endit(d, x): d.do_save = x d.accept() b = d.bb.addButton(_('&Save and Quit'), QDialogButtonBox.ButtonRole.ActionRole) b.setIcon(QIcon.ic('save.png')) connect_lambda(b.clicked, d, lambda d: endit(d, True)) b = d.bb.addButton(_('&Quit without saving'), QDialogButtonBox.ButtonRole.ActionRole) connect_lambda(b.clicked, d, lambda d: endit(d, False)) d.resize(d.sizeHint()) if d.exec() != QDialog.DialogCode.Accepted or d.do_save is None: return False if d.do_save: self.gui.action_save.trigger() self.start_terminal_save_indicator() return False return True def start_terminal_save_indicator(self): self.save_state() self.gui.blocking_job.set_msg(_('Saving, please wait...')) self.gui.blocking_job.start() self.doing_terminal_save = True def abort_terminal_save(self): self.doing_terminal_save = False self.gui.blocking_job.stop() def check_terminal_save(self): if self.doing_terminal_save and not self.save_manager.has_tasks: # terminal save could have been aborted self.shutdown() QApplication.instance().exit() def shutdown(self): self.save_state() completion_worker().shutdown() self.save_manager.check_for_completion.disconnect() self.gui.preview.stop_refresh_timer() self.gui.live_css.stop_update_timer() [x.reject() for x in _diff_dialogs] del _diff_dialogs[:] self.save_manager.shutdown() parse_worker.shutdown() self.save_manager.wait(0.1) def save_state(self): with self.editor_cache: self.save_book_edit_state() with tprefs: self.gui.save_state() def save_book_edit_state(self): c = current_container() if c and c.path_to_ebook: tprefs = self.editor_cache mem = tprefs['edit_book_state'] order = tprefs['edit_book_state_order'] extra = len(order) - 99 if extra > 0: order = [k for k in order[extra:] if k in mem] mem = {k:mem[k] for k in order} mem[c.path_to_ebook] = { 'editors':{name:ed.current_editing_state for name, ed in iteritems(editors)}, 'currently_editing':self.currently_editing, 'tab_order':self.gui.central.tab_order, } try: order.remove(c.path_to_ebook) except ValueError: pass order.append(c.path_to_ebook) tprefs['edit_book_state'] = mem tprefs['edit_book_state_order'] = order def restore_book_edit_state(self): c = current_container() if c and c.path_to_ebook: tprefs = self.editor_cache state = tprefs['edit_book_state'].get(c.path_to_ebook) if state is not None: opened = set() eds = state.get('editors', {}) for name in state.get('tab_order', ()): if c.has_name(name): try: editor = self.edit_file_requested(name) if editor is not None: opened.add(name) es = eds.get(name) if es is not None: editor.current_editing_state = es except Exception: import traceback traceback.print_exc() ce = state.get('currently_editing') if ce in opened: self.show_editor(ce)

    # }}}

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.gui2.tweak_book.plugin

Código fuente para calibre.gui2.tweak_book.plugin

#!/usr/bin/env python

__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'

import importlib
import sys

from qt.core import QToolButton

from calibre import prints
from calibre.customize import PluginInstallationType
from calibre.customize.ui import all_edit_book_tool_plugins
from calibre.gui2.tweak_book import current_container, tprefs
from calibre.gui2.tweak_book.boss import get_boss
from polyglot.builtins import itervalues

[documentos] class Tool: ''' The base class for individual tools in an Edit Book plugin. Useful members include: * ``self.plugin``: A reference to the :class:`calibre.customize.Plugin` object to which this tool belongs. * self. :attr:`boss` * self. :attr:`gui` Methods that must be overridden in sub classes: * :meth:`create_action` * :meth:`register_shortcut` ''' #: Set this to a unique name it will be used as a key name = None #: If True the user can choose to place this tool in the plugins toolbar allowed_in_toolbar = True #: If True the user can choose to place this tool in the plugins menu allowed_in_menu = True #: The popup mode for the menu (if any) of the toolbar button. Possible values are 'delayed', 'instant', 'button' toolbar_button_popup_mode = 'delayed' @property def boss(self): ' The :class:`calibre.gui2.tweak_book.boss.Boss` object. Used to control the user interface. ' return get_boss() @property def gui(self): ' The main window of the user interface ' return self.boss.gui @property def current_container(self): ' Return the current :class:`calibre.ebooks.oeb.polish.container.Container` object that represents the book being edited. ' return current_container()

[documentos] def register_shortcut(self, qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data): ''' Register a keyboard shortcut that will trigger the specified ``qaction``. This keyboard shortcut will become automatically customizable by the user in the Keyboard shortcuts section of the editor preferences. :param qaction: A QAction object, it will be triggered when the configured key combination is pressed by the user. :param unique_name: A unique name for this shortcut/action. It will be used internally, it must not be shared by any other actions in this plugin. :param default_keys: A list of the default keyboard shortcuts. If not specified no default shortcuts will be set. If the shortcuts specified here conflict with either builtin shortcuts or shortcuts from user configuration/other plugins, they will be ignored. In that case, users will have to configure the shortcuts manually via Preferences. For example: ``default_keys=('Ctrl+J', 'F9')``. :param short_text: An optional short description of this action. If not specified the text from the QAction will be used. :param description: An optional longer description of this action, it will be used in the preferences entry for this shortcut. ''' short_text = short_text or str(qaction.text()).replace('&&', '\0').replace('&', '').replace('\0', '&') self.gui.keyboard.register_shortcut( self.name + '_' + unique_name, short_text, default_keys=default_keys, action=qaction, description=description or '', group=_('Plugins'))

[documentos] def create_action(self, for_toolbar=True): ''' Create a QAction that will be added to either the plugins toolbar or the plugins menu depending on ``for_toolbar``. For example:: def create_action(self, for_toolbar=True): ac = QAction(get_icons('myicon.png'), 'Do something') if for_toolbar: # We want the toolbar button to have a popup menu menu = QMenu() ac.setMenu(menu) menu.addAction('Do something else') subaction = menu.addAction('And another') # Register a keyboard shortcut for this toolbar action be # careful to do this for only one of the toolbar action or # the menu action, not both. self.register_shortcut(ac, 'some-unique-name', default_keys=('Ctrl+K',)) return ac .. seealso:: Method :meth:`register_shortcut`. ''' raise NotImplementedError()

def load_plugin_tools(plugin):
    try:
        main = importlib.import_module(plugin.__class__.__module__+'.main')
    except ImportError:
        import traceback
        traceback.print_exc()
    else:
        for x in itervalues(vars(main)):
            if isinstance(x, type) and x is not Tool and issubclass(x, Tool):
                ans = x()
                ans.plugin = plugin
                yield ans

def plugin_action_sid(plugin, tool, for_toolbar=True):
    return plugin.name + tool.name + ('toolbar' if for_toolbar else 'menu')

plugin_toolbar_actions = []

def create_plugin_action(plugin, tool, for_toolbar, actions=None, toolbar_actions=None, plugin_menu_actions=None):
    try:
        ac = tool.create_action(for_toolbar=for_toolbar)
        if ac is None:
            raise RuntimeError('create_action() failed to return an action')
    except Exception:
        prints('Failed to create action for tool:', tool.name)
        import traceback
        traceback.print_exc()
        return
    sid = plugin_action_sid(plugin, tool, for_toolbar)
    if actions is not None and sid in actions:
        prints(f'The {tool.name} tool from the {plugin.name} plugin has a non unique name, ignoring')
    else:
        if actions is not None:
            actions[sid] = ac
        ac.sid = sid
        if for_toolbar:
            if toolbar_actions is not None:
                toolbar_actions[sid] = ac
                plugin_toolbar_actions.append(ac)
            ac.popup_mode = {'instant':QToolButton.ToolButtonPopupMode.InstantPopup, 'button':QToolButton.ToolButtonPopupMode.MenuButtonPopup}.get(
                tool.toolbar_button_popup_mode, QToolButton.ToolButtonPopupMode.DelayedPopup)
        else:
            if plugin_menu_actions is not None:
                plugin_menu_actions.append(ac)
    return ac

_tool_memory = []  # Needed to prevent the tool object from being garbage collected

def create_plugin_actions(actions, toolbar_actions, plugin_menu_actions):
    del _tool_memory[:]
    del plugin_toolbar_actions[:]

    for plugin in all_edit_book_tool_plugins():
        try:
            tools = tuple(load_plugin_tools(plugin))
        except Exception:
            if plugin.installation_type is PluginInstallationType.BUILTIN:
                raise
            print('Failed to load third-party plugin:', plugin.name, file=sys.stderr)
            import traceback
            traceback.print_exc()
            continue
        for tool in tools:
            _tool_memory.append(tool)
            if tool.allowed_in_toolbar:
                create_plugin_action(plugin, tool, True, actions, toolbar_actions, plugin_menu_actions)
            if tool.allowed_in_menu:
                create_plugin_action(plugin, tool, False, actions, toolbar_actions, plugin_menu_actions)

def install_plugin(plugin):
    for tool in load_plugin_tools(plugin):
        if tool.allowed_in_toolbar:
            sid = plugin_action_sid(plugin, tool, True)
            if sid not in tprefs['global_plugins_toolbar']:
                tprefs['global_plugins_toolbar'] = tprefs['global_plugins_toolbar'] + [sid]

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Navegación

módulos

Inicio »

Código de módulo »

calibre.utils.formatter_functions

Código fuente para calibre.utils.formatter_functions

#!/usr/bin/env python

'''
Created on 13 Jan 2011

@author: charles
'''

__license__   = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import inspect
import numbers
import posixpath
import re
import traceback
from contextlib import suppress
from datetime import datetime, timedelta
from enum import Enum, auto
from functools import partial
from math import ceil, floor, modf, trunc

from lxml import html

from calibre import human_readable, prepare_string_for_xml, prints
from calibre.constants import DEBUG
from calibre.db.constants import DATA_DIR_NAME, DATA_FILE_PATTERN
from calibre.db.notes.exim import expand_note_resources, parse_html
from calibre.ebooks.metadata import title_sort
from calibre.utils.config import tweaks
from calibre.utils.date import UNDEFINED_DATE, format_date, now, parse_date
from calibre.utils.icu import capitalize, sort_key, strcmp
from calibre.utils.icu import lower as icu_lower
from calibre.utils.localization import _, calibre_langcode_to_name, canonicalize_lang
from calibre.utils.titlecase import titlecase
from polyglot.builtins import iteritems, itervalues

class StoredObjectType(Enum):
    PythonFunction = auto()
    StoredGPMTemplate = auto()
    StoredPythonTemplate = auto()

class FormatterFunctions:

    error_function_body = ('def evaluate(self, formatter, kwargs, mi, locals):\n'
                       '\treturn "' +
                            _('Duplicate user function name {0}. '
                              'Change the name or ensure that the functions are identical') + '"')

    def __init__(self):
        self._builtins = {}
        self._functions = {}
        self._functions_from_library = {}

    def register_builtin(self, func_class):
        if not isinstance(func_class, FormatterFunction):
            raise ValueError('Class %s is not an instance of FormatterFunction'%(
                                    func_class.__class__.__name__))
        name = func_class.name
        if name in self._functions:
            raise ValueError('Name %s already used'%name)
        self._builtins[name] = func_class
        self._functions[name] = func_class
        for a in func_class.aliases:
            self._functions[a] = func_class

    def _register_function(self, func_class, replace=False):
        if not isinstance(func_class, FormatterFunction):
            raise ValueError('Class %s is not an instance of FormatterFunction'%(
                                    func_class.__class__.__name__))
        name = func_class.name
        if not replace and name in self._functions:
            raise ValueError('Name %s already used'%name)
        self._functions[name] = func_class

    def register_functions(self, library_uuid, funcs):
        self._functions_from_library[library_uuid] = funcs
        self._register_functions()

    def _register_functions(self):
        for compiled_funcs in itervalues(self._functions_from_library):
            for cls in compiled_funcs:
                f = self._functions.get(cls.name, None)
                replace = False
                if f is not None:
                    existing_body = f.program_text
                    new_body = cls.program_text
                    if new_body != existing_body:
                        # Change the body of the template function to one that will
                        # return an error message. Also change the arg count to
                        # -1 (variable) to avoid template compilation errors
                        if DEBUG:
                            print(f'attempt to replace formatter function {f.name} with a different body')
                        replace = True
                        func = [cls.name, '', -1, self.error_function_body.format(cls.name)]
                        cls = compile_user_function(*func)
                    else:
                        continue
                formatter_functions()._register_function(cls, replace=replace)

    def unregister_functions(self, library_uuid):
        if library_uuid in self._functions_from_library:
            for cls in self._functions_from_library[library_uuid]:
                self._functions.pop(cls.name, None)
            self._functions_from_library.pop(library_uuid)
            self._register_functions()

    def get_builtins(self):
        return self._builtins

    def get_builtins_and_aliases(self):
        res = {}
        for f in itervalues(self._builtins):
            res[f.name] = f
            for a in f.aliases:
                res[a] = f
        return res

    def get_functions(self):
        return self._functions

    def reset_to_builtins(self):
        self._functions = {}
        for n,c in self._builtins.items():
            self._functions[n] = c
            for a in c.aliases:
                self._functions[a] = c

_ff = FormatterFunctions()

def formatter_functions():
    global _ff
    return _ff

def only_in_gui_error(name):
    raise ValueError(_('The function {} can be used only in the GUI').format(name))

def get_database(mi, name):
    proxy = mi.get('_proxy_metadata', None)
    if proxy is None:
        if name is not None:
            only_in_gui_error(name)
        return None
    wr = proxy.get('_db', None)
    if wr is None:
        if name is not None:
            raise ValueError(_('In function {}: The database has been closed').format(name))
        return None
    cache = wr()
    if cache is None:
        if name is not None:
            raise ValueError(_('In function {}: The database has been closed').format(name))
        return None
    wr = getattr(cache, 'library_database_instance', None)
    if wr is None:
        if name is not None:
            only_in_gui_error()
        return None
    db = wr()
    if db is None:
        if name is not None:
            raise ValueError(_('In function {}: The database has been closed').format(name))
        return None
    return db

class FormatterFunction:

    doc = _('No documentation provided')
    name = 'no name provided'
    category = 'Unknown'
    arg_count = 0
    aliases = []
    object_type = StoredObjectType.PythonFunction

    def evaluate(self, formatter, kwargs, mi, locals, *args):
        raise NotImplementedError()

    def eval_(self, formatter, kwargs, mi, locals, *args):
        ret = self.evaluate(formatter, kwargs, mi, locals, *args)
        if isinstance(ret, (bytes, str)):
            return ret
        if isinstance(ret, list):
            return ','.join(ret)
        if isinstance(ret, (numbers.Number, bool)):
            return str(ret)

    def only_in_gui_error(self):
        only_in_gui_error(self.name)

    def get_database(self, mi):
        return get_database(mi, self.name)

class BuiltinFormatterFunction(FormatterFunction):

    def __init__(self):
        formatter_functions().register_builtin(self)
        eval_func = inspect.getmembers(self.__class__,
                        lambda x: inspect.isfunction(x) and x.__name__ == 'evaluate')
        try:
            lines = [l[4:] for l in inspect.getsourcelines(eval_func[0][1])[0]]
        except:
            lines = []
        self.program_text = ''.join(lines)

[documentos] class BuiltinStrcmp(BuiltinFormatterFunction): name = 'strcmp' arg_count = 5 category = 'Relational' __doc__ = doc = _('strcmp(x, y, lt, eq, gt) -- does a case-insensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt. In many cases the lexical comparison operators ' '(>, <, == etc) can replace this function.') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): v = strcmp(x, y) if v < 0: return lt if v == 0: return eq return gt

[documentos] class BuiltinStrcmpcase(BuiltinFormatterFunction): name = 'strcmpcase' arg_count = 5 category = 'Relational' __doc__ = doc = _('strcmpcase(x, y, lt, eq, gt) -- does a case-sensitive comparison of x ' 'and y as strings. Returns lt if x < y. Returns eq if x == y. ' 'Otherwise returns gt.\n' 'Note: This is NOT the default behavior used by calibre, for example, in the ' 'lexical comparison operators (==, >, <, etc.). This function could ' 'cause unexpected results, preferably use strcmp() whenever possible.') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): from calibre.utils.icu import case_sensitive_strcmp as case_strcmp v = case_strcmp(x, y) if v < 0: return lt if v == 0: return eq return gt

[documentos] class BuiltinCmp(BuiltinFormatterFunction): name = 'cmp' category = 'Relational' arg_count = 5 __doc__ = doc = _('cmp(x, y, lt, eq, gt) -- compares x and y after converting both to ' 'numbers. Returns lt if x < y. Returns eq if x == y. Otherwise returns gt. ' 'In many cases the numeric comparison operators ' '(>#, <#, ==# etc) can replace this function.') def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt): x = float(x if x and x != 'None' else 0) y = float(y if y and y != 'None' else 0) if x < y: return lt if x == y: return eq return gt

[documentos] class BuiltinFirstMatchingCmp(BuiltinFormatterFunction): name = 'first_matching_cmp' category = 'Relational' arg_count = -1 __doc__ = doc = _('first_matching_cmp(val, [cmp1, result1,]+, else_result) -- ' 'compares "val < cmpN" in sequence, returning resultN for ' 'the first comparison that succeeds. Returns else_result ' 'if no comparison succeeds. Example: ' 'first_matching_cmp(10,5,"small",10,"middle",15,"large","giant") ' 'returns "large". The same example with a first value of 16 returns "giant".') def evaluate(self, formatter, kwargs, mi, locals, *args): if (len(args) % 2) != 0: raise ValueError(_('first_matching_cmp requires an even number of arguments')) val = float(args[0] if args[0] and args[0] != 'None' else 0) for i in range(1, len(args) - 1, 2): c = float(args[i] if args[i] and args[i] != 'None' else 0) if val < c: return args[i+1] return args[len(args)-1]

[documentos] class BuiltinStrcat(BuiltinFormatterFunction): name = 'strcat' arg_count = -1 category = 'String manipulation' __doc__ = doc = _('strcat(a [, b]*) -- can take any number of arguments. Returns the ' 'string formed by concatenating all the arguments') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 res = '' for i in range(0, len(args)): res += args[i] return res

[documentos] class BuiltinStrlen(BuiltinFormatterFunction): name = 'strlen' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('strlen(a) -- Returns the length of the string passed as ' 'the argument') def evaluate(self, formatter, kwargs, mi, locals, a): try: return len(a) except: return -1

[documentos] class BuiltinAdd(BuiltinFormatterFunction): name = 'add' arg_count = -1 category = 'Arithmetic' __doc__ = doc = _('add(x [, y]*) -- returns the sum of its arguments. ' 'Throws an exception if an argument is not a number. ' 'This function can often be ' 'replaced with the + operator.') def evaluate(self, formatter, kwargs, mi, locals, *args): res = 0 for v in args: v = float(v if v and v != 'None' else 0) res += v return str(res)

[documentos] class BuiltinSubtract(BuiltinFormatterFunction): name = 'subtract' arg_count = 2 category = 'Arithmetic' __doc__ = doc = _('subtract(x, y) -- returns x - y. Throws an exception if ' 'either x or y are not numbers. This function can often be ' 'replaced with the - operator.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) y = float(y if y and y != 'None' else 0) return str(x - y)

[documentos] class BuiltinMultiply(BuiltinFormatterFunction): name = 'multiply' arg_count = -1 category = 'Arithmetic' __doc__ = doc = _('multiply(x [, y]*) -- returns the product of its arguments. ' 'Throws an exception if any argument is not a number. ' 'This function can often be replaced with the * operator.') def evaluate(self, formatter, kwargs, mi, locals, *args): res = 1 for v in args: v = float(v if v and v != 'None' else 0) res *= v return str(res)

[documentos] class BuiltinDivide(BuiltinFormatterFunction): name = 'divide' arg_count = 2 category = 'Arithmetic' __doc__ = doc = _('divide(x, y) -- returns x / y. Throws an exception if ' 'either x or y are not numbers.' ' This function can often be replaced with the / operator.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) y = float(y if y and y != 'None' else 0) return str(x / y)

[documentos] class BuiltinCeiling(BuiltinFormatterFunction): name = 'ceiling' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('ceiling(x) -- returns the smallest integer greater ' 'than or equal to x. Throws an exception if x is ' 'not a number.') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(ceil(x)))

[documentos] class BuiltinFloor(BuiltinFormatterFunction): name = 'floor' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('floor(x) -- returns the largest integer less ' 'than or equal to x. Throws an exception if x is ' 'not a number.') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(floor(x)))

[documentos] class BuiltinRound(BuiltinFormatterFunction): name = 'round' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('round(x) -- returns the nearest integer to x. ' 'Throws an exception if x is not a number.') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(int(round(x)))

[documentos] class BuiltinMod(BuiltinFormatterFunction): name = 'mod' arg_count = 2 category = 'Arithmetic' __doc__ = doc = _('mod(x) -- returns floor(remainder of x / y). ' 'Throws an exception if either x or y is not a number.') def evaluate(self, formatter, kwargs, mi, locals, x, y): x = float(x if x and x != 'None' else 0) y = float(y if y and y != 'None' else 0) return str(int(x % y))

[documentos] class BuiltinFractionalPart(BuiltinFormatterFunction): name = 'fractional_part' arg_count = 1 category = 'Arithmetic' __doc__ = doc = _('fractional_part(x) -- returns the value after the decimal ' 'point. For example, fractional_part(3.14) returns 0.14. ' 'Throws an exception if x is not a number.') def evaluate(self, formatter, kwargs, mi, locals, x): x = float(x if x and x != 'None' else 0) return str(modf(x)[0])

class BuiltinTemplate(BuiltinFormatterFunction):
    name = 'template'
    arg_count = 1
    category = 'Recursion'

    __doc__ = doc = _('template(x) -- evaluates x as a template. The evaluation is done '
            'in its own context, meaning that variables are not shared between '
            'the caller and the template evaluation. Because the { and } '
            'characters are special, you must use [[ for the { character and '
            ']] for the } character; they are converted automatically. '
            'For example, template(\'[[title_sort]]\') will evaluate the '
            'template {title_sort} and return its value. Note also that '
            'prefixes and suffixes (the `|prefix|suffix` syntax) cannot be '
            'used in the argument to this function when using template program mode.')

    def evaluate(self, formatter, kwargs, mi, locals, template):
        template = template.replace('[[', '{').replace(']]', '}')
        return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)

[documentos] class BuiltinEval(BuiltinFormatterFunction): name = 'eval' arg_count = 1 category = 'Recursion' __doc__ = doc = _('eval(template) -- evaluates the template, passing the local ' 'variables (those \'assign\'ed to) instead of the book metadata. ' ' This permits using the template processor to construct complex ' 'results from local variables. Because the { and } ' 'characters are special, you must use [[ for the { character and ' ']] for the } character; they are converted automatically. ' 'Note also that prefixes and suffixes (the `|prefix|suffix` syntax) ' 'cannot be used in the argument to this function when using ' 'template program mode.') def evaluate(self, formatter, kwargs, mi, locals, template): from calibre.utils.formatter import EvalFormatter template = template.replace('[[', '{').replace(']]', '}') return EvalFormatter().safe_format(template, locals, 'EVAL', None)

[documentos] class BuiltinAssign(BuiltinFormatterFunction): name = 'assign' arg_count = 2 category = 'Other' __doc__ = doc = _('assign(id, val) -- assigns val to id, then returns val. ' 'id must be an identifier, not an expression. ' 'This function can often be replaced with the = operator.') def evaluate(self, formatter, kwargs, mi, locals, target, value): locals[target] = value return value

[documentos] class BuiltinListSplit(BuiltinFormatterFunction): name = 'list_split' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('list_split(list_val, sep, id_prefix) -- splits the list_val ' "into separate values using 'sep', then assigns the values " "to variables named 'id_prefix_N' where N is the position " "of the value in the list. The first item has position 0 (zero). " "The function returns the last element in the list. " "Example: split('one:two:foo', ':', 'var') is equivalent " "to var_0 = 'one'; var_1 = 'two'; var_2 = 'foo'.") def evaluate(self, formatter, kwargs, mi, locals, list_val, sep, id_prefix): l = [v.strip() for v in list_val.split(sep)] res = '' for i,v in enumerate(l): res = locals[id_prefix+'_'+str(i)] = v return res

[documentos] class BuiltinPrint(BuiltinFormatterFunction): name = 'print' arg_count = -1 category = 'Other' __doc__ = doc = _('print(a[, b]*) -- prints the arguments to standard output. ' 'Unless you start calibre from the command line (calibre-debug -g), ' 'the output will go to a black hole.') def evaluate(self, formatter, kwargs, mi, locals, *args): print(args) return ''

[documentos] class BuiltinField(BuiltinFormatterFunction): name = 'field' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('field(lookup_name) -- returns the metadata field named by lookup_name') def evaluate(self, formatter, kwargs, mi, locals, name): return formatter.get_value(name, [], kwargs)

[documentos] class BuiltinRawField(BuiltinFormatterFunction): name = 'raw_field' arg_count = -1 category = 'Get values from metadata' __doc__ = doc = _('raw_field(lookup_name [, optional_default]) -- returns the ' 'metadata field named by lookup_name without applying any formatting. ' 'It evaluates and returns the optional second argument ' "'default' if the field is undefined ('None').") def evaluate(self, formatter, kwargs, mi, locals, name, default=None): res = getattr(mi, name, None) if res is None and default is not None: return default if isinstance(res, list): fm = mi.metadata_for_field(name) if fm is None: return ', '.join(res) return fm['is_multiple']['list_to_ui'].join(res) return str(res)

[documentos] class BuiltinRawList(BuiltinFormatterFunction): name = 'raw_list' arg_count = 2 category = 'Get values from metadata' __doc__ = doc = _('raw_list(lookup_name, separator) -- returns the metadata list ' 'named by lookup_name without applying any formatting or sorting and ' 'with items separated by separator.') def evaluate(self, formatter, kwargs, mi, locals, name, separator): res = getattr(mi, name, None) if not isinstance(res, list): return "%s is not a list" % name return separator.join(res)

[documentos] class BuiltinSubstr(BuiltinFormatterFunction): name = 'substr' arg_count = 3 category = 'String manipulation' __doc__ = doc = _('substr(str, start, end) -- returns the start\'th through the end\'th ' 'characters of str. The first character in str is the zero\'th ' 'character. If end is negative, then it indicates that many ' 'characters counting from the right. If end is zero, then it ' 'indicates the last character. For example, substr(\'12345\', 1, 0) ' 'returns \'2345\', and substr(\'12345\', 1, -1) returns \'234\'.') def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_): return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]

[documentos] class BuiltinLookup(BuiltinFormatterFunction): name = 'lookup' arg_count = -1 category = 'Iterating over values' __doc__ = doc = _('lookup(val, [pattern, field,]+ else_field) -- ' 'like switch, except the arguments are field (metadata) names, not ' 'text. The value of the appropriate field will be fetched and used. ' 'Note that because composite columns are fields, you can use this ' 'function in one composite field to use the value of some other ' 'composite field. This is extremely useful when constructing ' 'variable save paths') def evaluate(self, formatter, kwargs, mi, locals, val, *args): if len(args) == 2: # here for backwards compatibility if val: return formatter.vformat('{'+args[0].strip()+'}', [], kwargs) else: return formatter.vformat('{'+args[1].strip()+'}', [], kwargs) if (len(args) % 2) != 1: raise ValueError(_('lookup requires either 2 or an odd number of arguments')) i = 0 while i < len(args): if i + 1 >= len(args): return formatter.vformat('{' + args[i].strip() + '}', [], kwargs) if re.search(args[i], val, flags=re.I): return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs) i += 2

[documentos] class BuiltinTest(BuiltinFormatterFunction): name = 'test' arg_count = 3 category = 'If-then-else' __doc__ = doc = _('test(val, text if not empty, text if empty) -- return `text if not ' 'empty` if val is not empty, otherwise return `text if empty`') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set): if val: return value_if_set else: return value_not_set

[documentos] class BuiltinContains(BuiltinFormatterFunction): name = 'contains' arg_count = 4 category = 'If-then-else' __doc__ = doc = _('contains(val, pattern, text if match, text if not match) -- checks ' 'if val contains matches for the regular expression `pattern`. ' 'Returns `text if match` if matches are found, otherwise it returns ' '`text if no match`') def evaluate(self, formatter, kwargs, mi, locals, val, test, value_if_present, value_if_not): if re.search(test, val, flags=re.I): return value_if_present else: return value_if_not

[documentos] class BuiltinSwitch(BuiltinFormatterFunction): name = 'switch' arg_count = -1 category = 'Iterating over values' __doc__ = doc = _('switch(val, [pattern, value,]+ else_value) -- ' 'for each `pattern, value` pair, checks if `val` matches ' 'the regular expression `pattern` and if so, returns that ' '`value`. If no pattern matches, then `else_value` is returned. ' 'You can have as many `pattern, value` pairs as you want') def evaluate(self, formatter, kwargs, mi, locals, val, *args): if (len(args) % 2) != 1: raise ValueError(_('switch requires an even number of arguments')) i = 0 while i < len(args): if i + 1 >= len(args): return args[i] if re.search(args[i], val, flags=re.I): return args[i+1] i += 2

[documentos] class BuiltinSwitchIf(BuiltinFormatterFunction): name = 'switch_if' arg_count = -1 category = 'Iterating over values' __doc__ = doc = _('switch_if([test_expression, value_expression,]+ else_expression) -- ' 'for each "test_expression, value_expression" pair, checks if test_expression ' 'is True (non-empty) and if so returns the result of value_expression. ' 'If no test_expression is True then the result of else_expression is returned. ' 'You can have as many "test_expression, value_expression" pairs as you want.') def evaluate(self, formatter, kwargs, mi, locals, *args): if (len(args) % 2) != 1: raise ValueError(_('switch_if requires an odd number of arguments')) # We shouldn't get here because the function is inlined. However, someone # might call it directly. i = 0 while i < len(args): if i + 1 >= len(args): return args[i] if args[i]: return args[i+1] i += 2

[documentos] class BuiltinStrcatMax(BuiltinFormatterFunction): name = 'strcat_max' arg_count = -1 category = 'String manipulation' __doc__ = doc = _('strcat_max(max, string1 [, prefix2, string2]*) -- ' 'Returns a string formed by concatenating the arguments. The ' 'returned value is initialized to string1. `Prefix, string` ' 'pairs are added to the end of the value as long as the ' 'resulting string length is less than `max`. String1 is returned ' 'even if string1 is longer than max. You can pass as many ' '`prefix, string` pairs as you wish.') def evaluate(self, formatter, kwargs, mi, locals, *args): if len(args) < 2: raise ValueError(_('strcat_max requires 2 or more arguments')) if (len(args) % 2) != 0: raise ValueError(_('strcat_max requires an even number of arguments')) try: max = int(args[0]) except: raise ValueError(_('first argument to strcat_max must be an integer')) i = 2 result = args[1] try: while i < len(args): if (len(result) + len(args[i]) + len(args[i+1])) > max: break result = result + args[i] + args[i+1] i += 2 except: pass return result.strip()

[documentos] class BuiltinInList(BuiltinFormatterFunction): name = 'in_list' arg_count = -1 category = 'List lookup' __doc__ = doc = _('in_list(val, separator, [ pattern, found_val, ]+ not_found_val) -- ' 'treating val as a list of items separated by separator, ' 'if the pattern matches any of the list values then return found_val.' 'If the pattern matches no list value then return ' 'not_found_val. The pattern and found_value pairs can be repeated as ' 'many times as desired. The patterns are checked in order. The ' 'found_val for the first match is returned. ' 'Aliases: in_list(), list_contains()') aliases = ['list_contains'] def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args): if (len(args) % 2) != 1: raise ValueError(_('in_list requires an odd number of arguments')) l = [v.strip() for v in val.split(sep) if v.strip()] i = 0 while i < len(args): if i + 1 >= len(args): return args[i] sf = args[i] fv = args[i+1] if l: for v in l: if re.search(sf, v, flags=re.I): return fv i += 2

[documentos] class BuiltinStrInList(BuiltinFormatterFunction): name = 'str_in_list' arg_count = -1 category = 'List lookup' __doc__ = doc = _('str_in_list(val, separator, [string, found_val, ]+ not_found_val) -- ' 'treating val as a list of items separated by separator, if the ' 'string matches any of the list values then return found_val.' 'If the string matches no list value then return ' 'not_found_val. The comparison is exact match (not contains) and is ' 'case insensitive. The string and found_value pairs can be repeated as ' 'many times as desired. The patterns are checked in order. The ' 'found_val for the first match is returned.') def evaluate(self, formatter, kwargs, mi, locals, val, sep, *args): if (len(args) % 2) != 1: raise ValueError(_('str_in_list requires an odd number of arguments')) l = [v.strip() for v in val.split(sep) if v.strip()] i = 0 while i < len(args): if i + 1 >= len(args): return args[i] sf = args[i] fv = args[i+1] c = [v.strip() for v in sf.split(sep) if v.strip()] if l: for v in l: for t in c: if strcmp(t, v) == 0: return fv i += 2

[documentos] class BuiltinIdentifierInList(BuiltinFormatterFunction): name = 'identifier_in_list' arg_count = -1 category = 'List lookup' __doc__ = doc = _('identifier_in_list(val, id_name [, found_val, not_found_val]) -- ' 'treat val as a list of identifiers separated by commas. An identifier ' 'has the format "id_name:value". The id_name parameter is the id_name ' 'text to search for, either "id_name" or "id_name:regexp". The first case ' 'matches if there is any identifier matching that id_name. The second ' 'case matches if id_name matches an identifier and the regexp ' 'matches the identifier\'s value. If found_val and not_found_val ' 'are provided then if there is a match then return found_val, otherwise ' 'return not_found_val. If found_val and not_found_val are not ' 'provided then if there is a match then return the identifier:value ' 'pair, otherwise the empty string.') def evaluate(self, formatter, kwargs, mi, locals, val, ident, *args): if len(args) == 0: fv_is_id = True nfv = '' elif len(args) == 2: fv_is_id = False fv = args[0] nfv = args[1] else: raise ValueError(_("{} requires 2 or 4 arguments").format(self.name)) l = [v.strip() for v in val.split(',') if v.strip()] (id_, __, regexp) = ident.partition(':') if not id_: return nfv for candidate in l: i, __, v = candidate.partition(':') if v and i == id_: if not regexp or re.search(regexp, v, flags=re.I): return candidate if fv_is_id else fv return nfv

[documentos] class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 category = 'String manipulation' __doc__ = doc = _('re(val, pattern, replacement) -- return val after applying ' 'the regular expression. All instances of `pattern` are replaced ' 'with `replacement`. As in all of calibre, these are ' 'Python-compatible regular expressions') def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement): return re.sub(pattern, replacement, val, flags=re.I)

[documentos] class BuiltinReGroup(BuiltinFormatterFunction): name = 're_group' arg_count = -1 category = 'String manipulation' __doc__ = doc = _('re_group(val, pattern [, template_for_group]*) -- ' 'return a string made by applying the regular expression pattern ' 'to the val and replacing each matched instance with the string ' 'computed by replacing each matched group by the value returned ' 'by the corresponding template. The original matched value for the ' 'group is available as $. In template program mode, like for ' 'the template and the eval functions, you use [[ for { and ]] for }.' ' The following example in template program mode looks for series ' 'with more than one word and uppercases the first word: ' "{series:'re_group($, \"(\\S* )(.*)\", \"[[$:uppercase()]]\", \"[[$]]\")'}") def evaluate(self, formatter, kwargs, mi, locals, val, pattern, *args): from calibre.utils.formatter import EvalFormatter def repl(mo): res = '' if mo and mo.lastindex: for dex in range(0, mo.lastindex): gv = mo.group(dex+1) if gv is None: continue if len(args) > dex: template = args[dex].replace('[[', '{').replace(']]', '}') res += EvalFormatter().safe_format(template, {'$': gv}, 'EVAL', None, strip_results=False) else: res += gv return res return re.sub(pattern, repl, val, flags=re.I)

[documentos] class BuiltinSwapAroundComma(BuiltinFormatterFunction): name = 'swap_around_comma' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('swap_around_comma(val) -- given a value of the form ' '"B, A", return "A B". This is most useful for converting names ' 'in LN, FN format to FN LN. If there is no comma, the function ' 'returns val unchanged') def evaluate(self, formatter, kwargs, mi, locals, val): return re.sub(r'^(.*?),\s*(.*$)', r'\2 \1', val, flags=re.I).strip()

[documentos] class BuiltinIfempty(BuiltinFormatterFunction): name = 'ifempty' arg_count = 2 category = 'If-then-else' __doc__ = doc = _('ifempty(val, text if empty) -- return val if val is not empty, ' 'otherwise return `text if empty`') def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty): if val: return val else: return value_if_empty

[documentos] class BuiltinShorten(BuiltinFormatterFunction): name = 'shorten' arg_count = 4 category = 'String manipulation' __doc__ = doc = _('shorten(val, left chars, middle text, right chars) -- Return a ' 'shortened version of val, consisting of `left chars` ' 'characters from the beginning of val, followed by ' '`middle text`, followed by `right chars` characters from ' 'the end of the string. `Left chars` and `right chars` must be ' 'integers. For example, assume the title of the book is ' '`Ancient English Laws in the Times of Ivanhoe`, and you want ' 'it to fit in a space of at most 15 characters. If you use ' '{title:shorten(9,-,5)}, the result will be `Ancient E-anhoe`. ' 'If the field\'s length is less than left chars + right chars + ' 'the length of `middle text`, then the field will be used ' 'intact. For example, the title `The Dome` would not be changed.') def evaluate(self, formatter, kwargs, mi, locals, val, leading, center_string, trailing): l = max(0, int(leading)) t = max(0, int(trailing)) if len(val) > l + len(center_string) + t: return val[0:l] + center_string + ('' if t == 0 else val[-t:]) else: return val

[documentos] class BuiltinCount(BuiltinFormatterFunction): name = 'count' arg_count = 2 category = 'List manipulation' aliases = ['list_count'] __doc__ = doc = _('count(val, separator) -- interprets the value as a list of items ' 'separated by `separator`, returning the number of items in the ' 'list. Most lists use a comma as the separator, but authors ' 'uses an ampersand. Examples: {tags:count(,)}, {authors:count(&)}. ' 'Aliases: count(), list_count()') def evaluate(self, formatter, kwargs, mi, locals, val, sep): return str(len([v for v in val.split(sep) if v]))

[documentos] class BuiltinListCountMatching(BuiltinFormatterFunction): name = 'list_count_matching' arg_count = 3 category = 'List manipulation' aliases = ['count_matching'] __doc__ = doc = _('list_count_matching(list, pattern, separator) -- ' "interprets 'list' as a list of items separated by 'separator', " 'returning the number of items in the list that match the regular ' "expression 'pattern'. Aliases: list_count_matching(), count_matching()") def evaluate(self, formatter, kwargs, mi, locals, list_, pattern, sep): res = 0 for v in [x.strip() for x in list_.split(sep) if x.strip()]: if re.search(pattern, v, flags=re.I): res += 1 return str(res)

[documentos] class BuiltinListitem(BuiltinFormatterFunction): name = 'list_item' arg_count = 3 category = 'List lookup' __doc__ = doc = _('list_item(val, index, separator) -- interpret the value as a list of ' 'items separated by `separator`, returning the `index`th item. ' 'The first item is number zero. The last item can be returned ' 'using `list_item(-1,separator)`. If the item is not in the list, ' 'then the empty value is returned. The separator has the same ' 'meaning as in the count function.') def evaluate(self, formatter, kwargs, mi, locals, val, index, sep): if not val: return '' index = int(index) val = val.split(sep) try: return val[index].strip() except: return ''

[documentos] class BuiltinSelect(BuiltinFormatterFunction): name = 'select' arg_count = 2 category = 'List lookup' __doc__ = doc = _('select(val, key) -- interpret the value as a comma-separated list ' 'of items, with the items being "id:value". Find the pair with the ' 'id equal to key, and return the corresponding value. Returns the ' 'empty string if no match is found.' ) def evaluate(self, formatter, kwargs, mi, locals, val, key): if not val: return '' vals = [v.strip() for v in val.split(',')] tkey = key+':' for v in vals: if v.startswith(tkey): return v[len(tkey):] return ''

[documentos] class BuiltinApproximateFormats(BuiltinFormatterFunction): name = 'approximate_formats' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('approximate_formats() -- return a comma-separated ' 'list of formats that at one point were associated with the ' 'book. There is no guarantee that this list is correct, ' 'although it probably is. ' 'This function can be called in template program mode using ' 'the template "{:\'approximate_formats()\'}". ' 'Note that format names are always uppercase, as in EPUB. ' 'This function works only in the GUI. If you want to use these values ' 'in save-to-disk or send-to-device templates then you ' 'must make a custom "Column built from other columns", use ' 'the function in that column\'s template, and use that ' 'column\'s value in your save/send templates' ) def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): fmt_data = mi._proxy_metadata.db_approx_formats if not fmt_data: return '' data = sorted(fmt_data) return ','.join(v.upper() for v in data) self.only_in_gui_error()

[documentos] class BuiltinFormatsModtimes(BuiltinFormatterFunction): name = 'formats_modtimes' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('formats_modtimes(date_format) -- return a comma-separated ' 'list of colon-separated items representing modification times ' 'for the formats of a book. The date_format parameter ' 'specifies how the date is to be formatted. See the ' 'format_date function for details. You can use the select ' 'function to get the mod time for a specific ' 'format. Note that format names are always uppercase, ' 'as in EPUB.' ) def evaluate(self, formatter, kwargs, mi, locals, fmt): fmt_data = mi.get('format_metadata', {}) try: data = sorted(fmt_data.items(), key=lambda x:x[1]['mtime'], reverse=True) return ','.join(k.upper()+':'+format_date(v['mtime'], fmt) for k,v in data) except: return ''

[documentos] class BuiltinFormatsSizes(BuiltinFormatterFunction): name = 'formats_sizes' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('formats_sizes() -- return a comma-separated list of ' 'colon-separated items representing sizes in bytes ' 'of the formats of a book. You can use the select ' 'function to get the size for a specific ' 'format. Note that format names are always uppercase, ' 'as in EPUB.' ) def evaluate(self, formatter, kwargs, mi, locals): fmt_data = mi.get('format_metadata', {}) try: return ','.join(k.upper()+':'+str(v['size']) for k,v in iteritems(fmt_data)) except: return ''

[documentos] class BuiltinFormatsPaths(BuiltinFormatterFunction): name = 'formats_paths' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('formats_paths() -- return a comma-separated list of ' 'colon-separated items representing full path to ' 'the formats of a book. You can use the select ' 'function to get the path for a specific ' 'format. Note that format names are always uppercase, ' 'as in EPUB.') def evaluate(self, formatter, kwargs, mi, locals): fmt_data = mi.get('format_metadata', {}) try: return ','.join(k.upper()+':'+str(v['path']) for k,v in iteritems(fmt_data)) except: return ''

[documentos] class BuiltinHumanReadable(BuiltinFormatterFunction): name = 'human_readable' arg_count = 1 category = 'Formatting values' __doc__ = doc = _('human_readable(v) -- return a string ' 'representing the number v in KB, MB, GB, etc.' ) def evaluate(self, formatter, kwargs, mi, locals, val): try: return human_readable(round(float(val))) except: return ''

[documentos] class BuiltinFormatNumber(BuiltinFormatterFunction): name = 'format_number' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('format_number(v, template) -- format the number v using ' 'a Python formatting template such as "{0:5.2f}" or ' '"{0:,d}" or "${0:5,.2f}". The field_name part of the ' 'template must be a 0 (zero) (the "{0:" in the above examples). ' 'See the template language and Python documentation for more ' 'examples. You can leave off the leading "{0:" and trailing ' '"}" if the template contains only a format. Returns the empty ' 'string if formatting fails.' ) def evaluate(self, formatter, kwargs, mi, locals, val, template): if val == '' or val == 'None': return '' if '{' not in template: template = '{0:' + template + '}' try: v1 = float(val) except: return '' try: # Try formatting the value as a float return template.format(v1) except: pass try: # Try formatting the value as an int v2 = trunc(v1) if v2 == v1: return template.format(v2) except: pass return ''

[documentos] class BuiltinSublist(BuiltinFormatterFunction): name = 'sublist' arg_count = 4 category = 'List manipulation' __doc__ = doc = _('sublist(val, start_index, end_index, separator) -- interpret the ' 'value as a list of items separated by `separator`, returning a ' 'new list made from the `start_index` to the `end_index` item. ' 'The first item is number zero. If an index is negative, then it ' 'counts from the end of the list. As a special case, an end_index ' 'of zero is assumed to be the length of the list. Examples using ' 'basic template mode and assuming that the tags column (which is ' 'comma-separated) contains "A, B, C": ' '{tags:sublist(0,1,\\\\,)} returns "A". ' '{tags:sublist(-1,0,\\\\,)} returns "C". ' '{tags:sublist(0,-1,\\\\,)} returns "A, B".' ) def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep): if not val: return '' si = int(start_index) ei = int(end_index) # allow empty list items so counts are what the user expects val = [v.strip() for v in val.split(sep)] if sep == ',': sep = ', ' try: if ei == 0: return sep.join(val[si:]) else: return sep.join(val[si:ei]) except: return ''

[documentos] class BuiltinSubitems(BuiltinFormatterFunction): name = 'subitems' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('subitems(val, start_index, end_index) -- This function is used to ' 'break apart lists of items such as genres. It interprets the value ' 'as a comma-separated list of items, where each item is a period-' 'separated list. Returns a new list made by first finding all the ' 'period-separated items, then for each such item extracting the ' '`start_index` to the `end_index` components, then combining ' 'the results back together. The first component in a period-' 'separated list has an index of zero. If an index is negative, ' 'then it counts from the end of the list. As a special case, an ' 'end_index of zero is assumed to be the length of the list. ' 'Example using basic template mode and assuming a #genre value of ' '"A.B.C": {#genre:subitems(0,1)} returns "A". {#genre:subitems(0,2)} ' 'returns "A.B". {#genre:subitems(1,0)} returns "B.C". Assuming a #genre ' 'value of "A.B.C, D.E.F", {#genre:subitems(0,1)} returns "A, D". ' '{#genre:subitems(0,2)} returns "A.B, D.E"') period_pattern = re.compile(r'(?<=[^\.\s])\.(?=[^\.\s])', re.U) def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index): if not val: return '' si = int(start_index) ei = int(end_index) has_periods = '.' in val items = [v.strip() for v in val.split(',') if v.strip()] rv = set() for item in items: if has_periods and '.' in item: components = self.period_pattern.split(item) else: components = [item] try: if ei == 0: t = '.'.join(components[si:]).strip() else: t = '.'.join(components[si:ei]).strip() if t: rv.add(t) except: pass return ', '.join(sorted(rv, key=sort_key))

[documentos] class BuiltinFormatDate(BuiltinFormatterFunction): name = 'format_date' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('format_date(val, format_string) -- format the value, ' 'which must be a date, using the format_string, returning a string. ' 'The formatting codes are: ' 'd : the day as number without a leading zero (1 to 31) ' 'dd : the day as number with a leading zero (01 to 31) ' 'ddd : the abbreviated localized day name (e.g. "Mon" to "Sun"). ' 'dddd : the long localized day name (e.g. "Monday" to "Sunday"). ' 'M : the month as number without a leading zero (1 to 12). ' 'MM : the month as number with a leading zero (01 to 12) ' 'MMM : the abbreviated localized month name (e.g. "Jan" to "Dec"). ' 'MMMM : the long localized month name (e.g. "January" to "December"). ' 'yy : the year as two digit number (00 to 99). ' 'yyyy : the year as four digit number. ' 'h : the hours without a leading 0 (0 to 11 or 0 to 23, depending on am/pm) ' 'hh : the hours with a leading 0 (00 to 11 or 00 to 23, depending on am/pm) ' 'm : the minutes without a leading 0 (0 to 59) ' 'mm : the minutes with a leading 0 (00 to 59) ' 's : the seconds without a leading 0 (0 to 59) ' 'ss : the seconds with a leading 0 (00 to 59) ' 'ap : use a 12-hour clock instead of a 24-hour clock, with "ap" replaced by the localized string for am or pm ' 'AP : use a 12-hour clock instead of a 24-hour clock, with "AP" replaced by the localized string for AM or PM ' 'iso : the date with time and timezone. Must be the only format present ' 'to_number: the date as a floating point number ' 'from_number[:fmt]: format the timestamp using fmt if present otherwise iso') def evaluate(self, formatter, kwargs, mi, locals, val, format_string): if not val or val == 'None': return '' try: if format_string == 'to_number': s = parse_date(val).timestamp() elif format_string.startswith('from_number'): val = datetime.fromtimestamp(float(val)) f = format_string[12:] s = format_date(val, f if f else 'iso') else: s = format_date(parse_date(val), format_string) return s except: s = 'BAD DATE' return s

[documentos] class BuiltinFormatDateField(BuiltinFormatterFunction): name = 'format_date_field' arg_count = 2 category = 'Formatting values' __doc__ = doc = _("format_date_field(field_name, format_string) -- format " "the value in the field 'field_name', which must be the lookup name " "of date field, either standard or custom. See 'format_date' for " "the formatting codes. This function is much faster than format_date " "and should be used when you are formatting the value in a field " "(column). It can't be used for computed dates or dates in string " "variables. Example: format_date_field('pubdate', 'yyyy.MM.dd')") def evaluate(self, formatter, kwargs, mi, locals, field, format_string): try: if field not in mi.all_field_keys(): return _('Unknown field %s passed to function %s')%(field, 'format_date_field') val = mi.get(field, None) if val is None: s = '' elif format_string == 'to_number': s = val.timestamp() elif format_string.startswith('from_number'): val = datetime.fromtimestamp(float(val)) f = format_string[12:] s = format_date(val, f if f else 'iso') else: s = format_date(val, format_string) return s except: traceback.print_exc() s = 'BAD DATE' return s

[documentos] class BuiltinUppercase(BuiltinFormatterFunction): name = 'uppercase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('uppercase(val) -- return val in upper case') def evaluate(self, formatter, kwargs, mi, locals, val): return val.upper()

[documentos] class BuiltinLowercase(BuiltinFormatterFunction): name = 'lowercase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('lowercase(val) -- return val in lower case') def evaluate(self, formatter, kwargs, mi, locals, val): return val.lower()

[documentos] class BuiltinTitlecase(BuiltinFormatterFunction): name = 'titlecase' arg_count = 1 category = 'String case changes' __doc__ = doc = _('titlecase(val) -- return val in title case') def evaluate(self, formatter, kwargs, mi, locals, val): return titlecase(val)

[documentos] class BuiltinCapitalize(BuiltinFormatterFunction): name = 'capitalize' arg_count = 1 category = 'String case changes' __doc__ = doc = _('capitalize(val) -- return val capitalized') def evaluate(self, formatter, kwargs, mi, locals, val): return capitalize(val)

[documentos] class BuiltinBooksize(BuiltinFormatterFunction): name = 'booksize' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('booksize() -- return value of the size field. ' 'This function works only in the GUI. If you want to use this value ' 'in save-to-disk or send-to-device templates then you ' 'must make a custom "Column built from other columns", use ' 'the function in that column\'s template, and use that ' 'column\'s value in your save/send templates') def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): try: v = mi._proxy_metadata.book_size if v is not None: return str(mi._proxy_metadata.book_size) return '' except: pass return '' self.only_in_gui_error()

[documentos] class BuiltinOndevice(BuiltinFormatterFunction): name = 'ondevice' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('ondevice() -- return Yes if ondevice is set, otherwise return ' 'the empty string. This function works only in the GUI. If you want to ' 'use this value in save-to-disk or send-to-device templates then you ' 'must make a custom "Column built from other columns", use ' 'the function in that column\'s template, and use that ' 'column\'s value in your save/send templates') def evaluate(self, formatter, kwargs, mi, locals): if hasattr(mi, '_proxy_metadata'): if mi._proxy_metadata.ondevice_col: return _('Yes') return '' self.only_in_gui_error()

[documentos] class BuiltinAnnotationCount(BuiltinFormatterFunction): name = 'annotation_count' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('annotation_count() -- return the total number of annotations ' 'of all types attached to the current book. ' 'This function works only in the GUI.') def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).new_api.annotation_count_for_book(mi.id) return '' if c == 0 else str(c)

[documentos] class BuiltinIsMarked(BuiltinFormatterFunction): name = 'is_marked' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _("is_marked() -- check whether the book is 'marked' in " "calibre. If it is then return the value of the mark, " "either 'true' or the comma-separated list of named " "marks. Returns '' if the book is not marked.") def evaluate(self, formatter, kwargs, mi, locals): c = self.get_database(mi).data.get_marked(mi.id) return c if c else ''

[documentos] class BuiltinSeriesSort(BuiltinFormatterFunction): name = 'series_sort' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('series_sort() -- return the series sort value') def evaluate(self, formatter, kwargs, mi, locals): if mi.series: langs = mi.languages lang = langs[0] if langs else None return title_sort(mi.series, lang=lang) return ''

[documentos] class BuiltinHasCover(BuiltinFormatterFunction): name = 'has_cover' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('has_cover() -- return Yes if the book has a cover, ' 'otherwise return the empty string') def evaluate(self, formatter, kwargs, mi, locals): if mi.has_cover: return _('Yes') return ''

[documentos] class BuiltinFirstNonEmpty(BuiltinFormatterFunction): name = 'first_non_empty' arg_count = -1 category = 'Iterating over values' __doc__ = doc = _('first_non_empty(value [, value]*) -- ' 'returns the first value that is not empty. If all values are ' 'empty, then the empty string is returned. ' 'You can have as many values as you want.') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 while i < len(args): if args[i]: return args[i] i += 1 return ''

[documentos] class BuiltinAnd(BuiltinFormatterFunction): name = 'and' arg_count = -1 category = 'Boolean' __doc__ = doc = _('and(value [, value]*) -- ' 'returns the string "1" if all values are not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want. In many ' 'cases the && operator can replace this function.') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 while i < len(args): if not args[i]: return '' i += 1 return '1'

[documentos] class BuiltinOr(BuiltinFormatterFunction): name = 'or' arg_count = -1 category = 'Boolean' __doc__ = doc = _('or(value [, value]*) -- ' 'returns the string "1" if any value is not empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. You can have as many values as you want. In many ' 'cases the || operator can replace this function.') def evaluate(self, formatter, kwargs, mi, locals, *args): i = 0 while i < len(args): if args[i]: return '1' i += 1 return ''

[documentos] class BuiltinNot(BuiltinFormatterFunction): name = 'not' arg_count = 1 category = 'Boolean' __doc__ = doc = _('not(value) -- ' 'returns the string "1" if the value is empty, otherwise ' 'returns the empty string. This function works well with test or ' 'first_non_empty. In many cases the ! operator can replace this ' 'function.') def evaluate(self, formatter, kwargs, mi, locals, val): return '' if val else '1'

[documentos] class BuiltinListJoin(BuiltinFormatterFunction): name = 'list_join' arg_count = -1 category = 'List manipulation' __doc__ = doc = _("list_join(with_separator, list1, separator1 [, list2, separator2]*) -- " "return a list made by joining the items in the source lists " "(list1, etc) using with_separator between the items in the " "result list. Items in each source list[123...] are separated " "by the associated separator[123...]. A list can contain " "zero values. It can be a field like publisher that is " "single-valued, effectively a one-item list. Duplicates " "are removed using a case-insensitive comparison. Items are " "returned in the order they appear in the source lists. " "If items on lists differ only in letter case then the last " "is used. All separators can be more than one character.\n" "Example:") + "\n\n" + ( " program:\n" " list_join('#@#', $authors, '&', $tags, ',')\n\n") + _( "You can use list_join on the results of previous " "calls to list_join as follows:") + "\n" + ( " program:\n\n" " a = list_join('#@#', $authors, '&', $tags, ',');\n" " b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&')\n\n") + _( "You can use expressions to generate a list. For example, " "assume you want items for authors and #genre, but " "with the genre changed to the word 'Genre: ' followed by " "the first letter of the genre, i.e. the genre 'Fiction' " "becomes 'Genre: F'. The following will do that:") + "\n" + ( " program:\n" " list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \\1'), ',')") def evaluate(self, formatter, kwargs, mi, locals, with_separator, *args): if len(args) % 2 != 0: raise ValueError( _("Invalid 'List, separator' pairs. Every list must have one " "associated separator")) # Starting in python 3.7 dicts preserve order so we don't need OrderedDict result = dict() i = 0 while i < len(args): lst = [v.strip() for v in args[i].split(args[i+1]) if v.strip()] result.update({item.lower():item for item in lst}) i += 2 return with_separator.join(result.values())

[documentos] class BuiltinListUnion(BuiltinFormatterFunction): name = 'list_union' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('list_union(list1, list2, separator) -- ' 'return a list made by merging the items in list1 and list2, ' 'removing duplicate items using a case-insensitive comparison. If ' 'items differ in case, the one in list1 is used. ' 'The items in list1 and list2 are separated by separator, as are ' 'the items in the returned list. Aliases: list_union(), merge_lists()') aliases = ['merge_lists'] def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): res = {icu_lower(l.strip()): l.strip() for l in list2.split(separator) if l.strip()} res.update({icu_lower(l.strip()): l.strip() for l in list1.split(separator) if l.strip()}) if separator == ',': separator = ', ' return separator.join(res.values())

[documentos] class BuiltinRange(BuiltinFormatterFunction): name = 'range' arg_count = -1 category = 'List manipulation' __doc__ = doc = _("range(start, stop, step, limit) -- " "returns a list of numbers generated by looping over the " "range specified by the parameters start, stop, and step, " "with a maximum length of limit. The first value produced " "is 'start'. Subsequent values next_v are " "current_v+step. The loop continues while " "next_v < stop assuming step is positive, otherwise " "while next_v > stop. An empty list is produced if " "start fails the test: start>=stop if step " "is positive. The limit sets the maximum length of " "the list and has a default of 1000. The parameters " "start, step, and limit are optional. " "Calling range() with one argument specifies stop. " "Two arguments specify start and stop. Three arguments " "specify start, stop, and step. Four " "arguments specify start, stop, step and limit. " "Examples: range(5) -> '0,1,2,3,4'. range(0,5) -> '0,1,2,3,4'. " "range(-1,5) -> '-1,0,1,2,3,4'. range(1,5) -> '1,2,3,4'. " "range(1,5,2) -> '1,3'. range(1,5,2,5) -> '1,3'. " "range(1,5,2,1) -> error(limit exceeded).") def evaluate(self, formatter, kwargs, mi, locals, *args): limit_val = 1000 start_val = 0 step_val = 1 if len(args) == 1: stop_val = int(args[0] if args[0] and args[0] != 'None' else 0) elif len(args) == 2: start_val = int(args[0] if args[0] and args[0] != 'None' else 0) stop_val = int(args[1] if args[1] and args[1] != 'None' else 0) elif len(args) >= 3: start_val = int(args[0] if args[0] and args[0] != 'None' else 0) stop_val = int(args[1] if args[1] and args[1] != 'None' else 0) step_val = int(args[2] if args[2] and args[2] != 'None' else 0) if len(args) > 3: limit_val = int(args[3] if args[3] and args[3] != 'None' else 0) r = range(start_val, stop_val, step_val) if len(r) > limit_val: raise ValueError( _("{0}: length ({1}) longer than limit ({2})").format( 'range', len(r), str(limit_val))) return ', '.join([str(v) for v in r])

[documentos] class BuiltinListRemoveDuplicates(BuiltinFormatterFunction): name = 'list_remove_duplicates' arg_count = 2 category = 'List manipulation' __doc__ = doc = _('list_remove_duplicates(list, separator) -- ' 'return a list made by removing duplicate items in the source list. ' 'If items differ only in case, the last of them is returned. ' 'The items in source list are separated by separator, as are ' 'the items in the returned list.') def evaluate(self, formatter, kwargs, mi, locals, list_, separator): res = {icu_lower(l.strip()): l.strip() for l in list_.split(separator) if l.strip()} if separator == ',': separator = ', ' return separator.join(res.values())

[documentos] class BuiltinListDifference(BuiltinFormatterFunction): name = 'list_difference' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('list_difference(list1, list2, separator) -- ' 'return a list made by removing from list1 any item found in list2, ' 'using a case-insensitive comparison. The items in list1 and list2 ' 'are separated by separator, as are the items in the returned list.') def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): l1 = [l.strip() for l in list1.split(separator) if l.strip()] l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()} res = [] for i in l1: if icu_lower(i) not in l2 and i not in res: res.append(i) if separator == ',': return ', '.join(res) return separator.join(res)

[documentos] class BuiltinListIntersection(BuiltinFormatterFunction): name = 'list_intersection' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('list_intersection(list1, list2, separator) -- ' 'return a list made by removing from list1 any item not found in list2, ' 'using a case-insensitive comparison. The items in list1 and list2 ' 'are separated by separator, as are the items in the returned list.') def evaluate(self, formatter, kwargs, mi, locals, list1, list2, separator): l1 = [l.strip() for l in list1.split(separator) if l.strip()] l2 = {icu_lower(l.strip()) for l in list2.split(separator) if l.strip()} res = [] for i in l1: if icu_lower(i) in l2 and i not in res: res.append(i) if separator == ',': return ', '.join(res) return separator.join(res)

[documentos] class BuiltinListSort(BuiltinFormatterFunction): name = 'list_sort' arg_count = 3 category = 'List manipulation' __doc__ = doc = _('list_sort(list, direction, separator) -- ' 'return list sorted using a case-insensitive sort. If direction is ' 'zero, the list is sorted ascending, otherwise descending. The list items ' 'are separated by separator, as are the items in the returned list.') def evaluate(self, formatter, kwargs, mi, locals, list1, direction, separator): res = [l.strip() for l in list1.split(separator) if l.strip()] if separator == ',': return ', '.join(sorted(res, key=sort_key, reverse=direction != "0")) return separator.join(sorted(res, key=sort_key, reverse=direction != "0"))

[documentos] class BuiltinListEquals(BuiltinFormatterFunction): name = 'list_equals' arg_count = 6 category = 'List manipulation' __doc__ = doc = _('list_equals(list1, sep1, list2, sep2, yes_val, no_val) -- ' 'return yes_val if list1 and list2 contain the same items, ' 'otherwise return no_val. The items are determined by splitting ' 'each list using the appropriate separator character (sep1 or ' 'sep2). The order of items in the lists is not relevant. ' 'The comparison is case insensitive.') def evaluate(self, formatter, kwargs, mi, locals, list1, sep1, list2, sep2, yes_val, no_val): s1 = {icu_lower(l.strip()) for l in list1.split(sep1) if l.strip()} s2 = {icu_lower(l.strip()) for l in list2.split(sep2) if l.strip()} if s1 == s2: return yes_val return no_val

[documentos] class BuiltinListRe(BuiltinFormatterFunction): name = 'list_re' arg_count = 4 category = 'List manipulation' __doc__ = doc = _('list_re(src_list, separator, include_re, opt_replace) -- ' 'Construct a list by first separating src_list into items using ' 'the separator character. For each item in the list, check if it ' 'matches include_re. If it does, then add it to the list to be ' 'returned. If opt_replace is not the empty string, then apply the ' 'replacement before adding the item to the returned list.') def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, opt_replace): l = [l.strip() for l in src_list.split(separator) if l.strip()] res = [] for item in l: if re.search(include_re, item, flags=re.I) is not None: if opt_replace: item = re.sub(include_re, opt_replace, item) for i in [t.strip() for t in item.split(separator) if t.strip()]: if i not in res: res.append(i) if separator == ',': return ', '.join(res) return separator.join(res)

[documentos] class BuiltinListReGroup(BuiltinFormatterFunction): name = 'list_re_group' arg_count = -1 category = 'List manipulation' __doc__ = doc = _('list_re_group(src_list, separator, include_re, search_re [, group_template]+) -- ' 'Like list_re except replacements are not optional. It ' 'uses re_group(list_item, search_re, group_template, ...) when ' 'doing the replacements on the resulting list.') def evaluate(self, formatter, kwargs, mi, locals, src_list, separator, include_re, search_re, *args): from calibre.utils.formatter import EvalFormatter l = [l.strip() for l in src_list.split(separator) if l.strip()] res = [] for item in l: def repl(mo): newval = '' if mo and mo.lastindex: for dex in range(0, mo.lastindex): gv = mo.group(dex+1) if gv is None: continue if len(args) > dex: template = args[dex].replace('[[', '{').replace(']]', '}') newval += EvalFormatter().safe_format(template, {'$': gv}, 'EVAL', None, strip_results=False) else: newval += gv return newval if re.search(include_re, item, flags=re.I) is not None: item = re.sub(search_re, repl, item, flags=re.I) for i in [t.strip() for t in item.split(separator) if t.strip()]: if i not in res: res.append(i) if separator == ',': return ', '.join(res) return separator.join(res)

[documentos] class BuiltinToday(BuiltinFormatterFunction): name = 'today' arg_count = 0 category = 'Date functions' __doc__ = doc = _('today() -- ' 'return a date string for today. This value is designed for use in ' 'format_date or days_between, but can be manipulated like any ' 'other string. The date is in ISO format.') def evaluate(self, formatter, kwargs, mi, locals): return format_date(now(), 'iso')

[documentos] class BuiltinDaysBetween(BuiltinFormatterFunction): name = 'days_between' arg_count = 2 category = 'Date functions' __doc__ = doc = _('days_between(date1, date2) -- ' 'return the number of days between date1 and date2. The number is ' 'positive if date1 is greater than date2, otherwise negative. If ' 'either date1 or date2 are not dates, the function returns the ' 'empty string.') def evaluate(self, formatter, kwargs, mi, locals, date1, date2): try: d1 = parse_date(date1) if d1 == UNDEFINED_DATE: return '' d2 = parse_date(date2) if d2 == UNDEFINED_DATE: return '' except: return '' i = d1 - d2 return '%.1f'%(i.days + (i.seconds/(24.0*60.0*60.0)))

[documentos] class BuiltinDateArithmetic(BuiltinFormatterFunction): name = 'date_arithmetic' arg_count = -1 category = 'Date functions' __doc__ = doc = _('date_arithmetic(date, calc_spec, fmt) -- ' "Calculate a new date from 'date' using 'calc_spec'. Return the " "new date formatted according to optional 'fmt': if not supplied " "then the result will be in iso format. The calc_spec is a string " "formed by concatenating pairs of 'vW' (valueWhat) where 'v' is a " "possibly-negative number and W is one of the following letters: " "s: add 'v' seconds to 'date' " "m: add 'v' minutes to 'date' " "h: add 'v' hours to 'date' " "d: add 'v' days to 'date' " "w: add 'v' weeks to 'date' " "y: add 'v' years to 'date', where a year is 365 days. " "Example: '1s3d-1m' will add 1 second, add 3 days, and subtract 1 " "minute from 'date'.") calc_ops = { 's': lambda v: timedelta(seconds=v), 'm': lambda v: timedelta(minutes=v), 'h': lambda v: timedelta(hours=v), 'd': lambda v: timedelta(days=v), 'w': lambda v: timedelta(weeks=v), 'y': lambda v: timedelta(days=v * 365), } def evaluate(self, formatter, kwargs, mi, locals, date, calc_spec, fmt=None): try: d = parse_date(date) if d == UNDEFINED_DATE: return '' while calc_spec: mo = re.match(r'([-+\d]+)([smhdwy])', calc_spec) if mo is None: raise ValueError( _("{0}: invalid calculation specifier '{1}'").format( 'date_arithmetic', calc_spec)) d += self.calc_ops[mo[2]](int(mo[1])) calc_spec = calc_spec[len(mo[0]):] return format_date(d, fmt if fmt else 'iso') except ValueError as e: raise e except Exception as e: traceback.print_exc() raise ValueError(_("{0}: error: {1}").format('date_arithmetic', str(e)))

[documentos] class BuiltinLanguageStrings(BuiltinFormatterFunction): name = 'language_strings' arg_count = 2 category = 'Get values from metadata' __doc__ = doc = _('language_strings(lang_codes, localize) -- ' 'return the strings for the language codes passed in lang_codes. ' 'If localize is zero, return the strings in English. If ' 'localize is not zero, return the strings in the language of ' 'the current locale. Lang_codes is a comma-separated list.') def evaluate(self, formatter, kwargs, mi, locals, lang_codes, localize): retval = [] for c in [c.strip() for c in lang_codes.split(',') if c.strip()]: try: n = calibre_langcode_to_name(c, localize != '0') if n: retval.append(n) except: pass return ', '.join(retval)

[documentos] class BuiltinLanguageCodes(BuiltinFormatterFunction): name = 'language_codes' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('language_codes(lang_strings) -- ' 'return the language codes for the strings passed in lang_strings. ' 'The strings must be in the language of the current locale. ' 'Lang_strings is a comma-separated list.') def evaluate(self, formatter, kwargs, mi, locals, lang_strings): retval = [] for c in [c.strip() for c in lang_strings.split(',') if c.strip()]: try: cv = canonicalize_lang(c) if cv: retval.append(canonicalize_lang(cv)) except: pass return ', '.join(retval)

[documentos] class BuiltinCurrentLibraryName(BuiltinFormatterFunction): name = 'current_library_name' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('current_library_name() -- ' 'return the last name on the path to the current calibre library. ' 'This function can be called in template program mode using the ' 'template "{:\'current_library_name()\'}".') def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_name return current_library_name()

[documentos] class BuiltinCurrentLibraryPath(BuiltinFormatterFunction): name = 'current_library_path' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('current_library_path() -- ' 'return the path to the current calibre library. This function can ' 'be called in template program mode using the template ' '"{:\'current_library_path()\'}".') def evaluate(self, formatter, kwargs, mi, locals): from calibre.library import current_library_path return current_library_path()

[documentos] class BuiltinFinishFormatting(BuiltinFormatterFunction): name = 'finish_formatting' arg_count = 4 category = 'Formatting values' __doc__ = doc = _('finish_formatting(val, fmt, prefix, suffix) -- apply the ' 'format, prefix, and suffix to a value in the same way as ' 'done in a template like `{series_index:05.2f| - |- }`. For ' 'example, the following program produces the same output ' 'as the above template: ' 'program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")') def evaluate(self, formatter, kwargs, mi, locals_, val, fmt, prefix, suffix): if not val: return val return prefix + formatter._do_format(val, fmt) + suffix

[documentos] class BuiltinVirtualLibraries(BuiltinFormatterFunction): name = 'virtual_libraries' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('virtual_libraries() -- return a comma-separated list of ' 'Virtual libraries that contain this book. This function ' 'works only in the GUI. If you want to use these values ' 'in save-to-disk or send-to-device templates then you ' 'must make a custom "Column built from other columns", use ' 'the function in that column\'s template, and use that ' 'column\'s value in your save/send templates') def evaluate(self, formatter, kwargs, mi, locals_): db = self.get_database(mi) try: a = db.data.get_virtual_libraries_for_books((mi.id,)) return ', '.join(a[mi.id]) except ValueError as v: return str(v)

[documentos] class BuiltinCurrentVirtualLibraryName(BuiltinFormatterFunction): name = 'current_virtual_library_name' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('current_virtual_library_name() -- ' 'return the name of the current virtual library if there is one, ' 'otherwise the empty string. Library name case is preserved. ' 'Example: "program: current_virtual_library_name()".') def evaluate(self, formatter, kwargs, mi, locals): return self.get_database(mi).data.get_base_restriction_name()

[documentos] class BuiltinUserCategories(BuiltinFormatterFunction): name = 'user_categories' arg_count = 0 category = 'Get values from metadata' __doc__ = doc = _('user_categories() -- return a comma-separated list of ' 'the user categories that contain this book. This function ' 'works only in the GUI. If you want to use these values ' 'in save-to-disk or send-to-device templates then you ' 'must make a custom "Column built from other columns", use ' 'the function in that column\'s template, and use that ' 'column\'s value in your save/send templates') def evaluate(self, formatter, kwargs, mi, locals_): if hasattr(mi, '_proxy_metadata'): cats = {k for k, v in iteritems(mi._proxy_metadata.user_categories) if v} cats = sorted(cats, key=sort_key) return ', '.join(cats) self.only_in_gui_error()

[documentos] class BuiltinTransliterate(BuiltinFormatterFunction): name = 'transliterate' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('transliterate(a) -- Returns a string in a latin alphabet ' 'formed by approximating the sound of the words in the ' 'source string. For example, if the source is "{0}"' ' the function returns "{1}".').format( "Фёдор Миха́йлович Достоевский", 'Fiodor Mikhailovich Dostoievskii') def evaluate(self, formatter, kwargs, mi, locals, source): from calibre.utils.filenames import ascii_text return ascii_text(source)

[documentos] class BuiltinGetLink(BuiltinFormatterFunction): name = 'get_link' arg_count = 2 category = 'Template database functions' __doc__ = doc = _("get_link(field_name, field_value) -- fetch the link for " "field 'field_name' with value 'field_value'. If there is " "no attached link, return ''. Example: " "get_link('tags', 'Fiction') returns the link attached to " "the tag 'Fiction'.") def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api try: link = None item_id = db.get_item_id(field_name, field_value) if item_id is not None: link = db.link_for(field_name, item_id) return link if link is not None else '' except Exception as e: traceback.print_exc() raise ValueError(e)

[documentos] class BuiltinAuthorLinks(BuiltinFormatterFunction): name = 'author_links' arg_count = 2 category = 'Get values from metadata' __doc__ = doc = _('author_links(val_separator, pair_separator) -- returns ' 'a string containing a list of authors and that author\'s ' 'link values in the ' 'form author1 val_separator author1link pair_separator ' 'author2 val_separator author2link etc. An author is ' 'separated from its link value by the val_separator string ' 'with no added spaces. author:linkvalue pairs are separated ' 'by the pair_separator string argument with no added spaces. ' 'It is up to you to choose separator strings that do ' 'not occur in author names or links. An author is ' 'included even if the author link is empty.') def evaluate(self, formatter, kwargs, mi, locals, val_sep, pair_sep): if hasattr(mi, '_proxy_metadata'): link_data = mi._proxy_metadata.link_maps if not link_data: return '' link_data = link_data.get('authors') if not link_data: return '' names = sorted(link_data.keys(), key=sort_key) return pair_sep.join(n + val_sep + link_data[n] for n in names) self.only_in_gui_error()

[documentos] class BuiltinAuthorSorts(BuiltinFormatterFunction): name = 'author_sorts' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _('author_sorts(val_separator) -- returns a string ' 'containing a list of author\'s sort values for the ' 'authors of the book. The sort is the one in the author ' 'metadata (different from the author_sort in books). The ' 'returned list has the form author sort 1 val_separator ' 'author sort 2 etc. The author sort values in this list ' 'are in the same order as the authors of the book. If ' 'you want spaces around val_separator then include them ' 'in the separator string') def evaluate(self, formatter, kwargs, mi, locals, val_sep): sort_data = mi.author_sort_map if not sort_data: return '' names = [sort_data.get(n) for n in mi.authors if n.strip()] return val_sep.join(n for n in names)

[documentos] class BuiltinConnectedDeviceName(BuiltinFormatterFunction): name = 'connected_device_name' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _("connected_device_name(storage_location) -- if a device is " "connected then return the device name, otherwise return " "the empty string. Each storage location on a device can " "have a different name. The location names are 'main', " "'carda' and 'cardb'. This function works only in the GUI.") def evaluate(self, formatter, kwargs, mi, locals, storage_location): # We can't use get_database() here because we need the device manager. # In other words, the function really does need the GUI with suppress(Exception): # Do the import here so that we don't entangle the GUI when using # command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information() if info is None: return '' try: if storage_location not in {'main', 'carda', 'cardb'}: raise ValueError( _('connected_device_name: invalid storage location "{}"' .format(storage_location))) info = info['info'][4] if storage_location not in info: return '' return info[storage_location]['device_name'] except Exception: traceback.print_exc() raise self.only_in_gui_error()

[documentos] class BuiltinConnectedDeviceUUID(BuiltinFormatterFunction): name = 'connected_device_uuid' arg_count = 1 category = 'Get values from metadata' __doc__ = doc = _("connected_device_uuid(storage_location) -- if a device is " "connected then return the device uuid (unique id), " "otherwise return the empty string. Each storage location " "on a device has a different uuid. The location names are " "'main', 'carda' and 'cardb'. This function works only in " "the GUI.") def evaluate(self, formatter, kwargs, mi, locals, storage_location): # We can't use get_database() here because we need the device manager. # In other words, the function really does need the GUI with suppress(Exception): # Do the import here so that we don't entangle the GUI when using # command line functions from calibre.gui2.ui import get_gui info = get_gui().device_manager.get_current_device_information() if info is None: return '' try: if storage_location not in {'main', 'carda', 'cardb'}: raise ValueError( _('connected_device_name: invalid storage location "{}"' .format(storage_location))) info = info['info'][4] if storage_location not in info: return '' return info[storage_location]['device_store_uuid'] except Exception: traceback.print_exc() raise self.only_in_gui_error()

[documentos] class BuiltinCheckYesNo(BuiltinFormatterFunction): name = 'check_yes_no' arg_count = 4 category = 'If-then-else' __doc__ = doc = _('check_yes_no(field_name, is_undefined, is_false, is_true) ' '-- checks the value of the yes/no field named by the ' 'lookup key field_name for a value specified by the ' 'parameters, returning "yes" if a match is found, otherwise ' 'returning an empty string. Set the parameter is_undefined, ' 'is_false, or is_true to 1 (the number) to check that ' 'condition, otherwise set it to 0. Example: ' 'check_yes_no("#bool", 1, 0, 1) returns "yes" if the ' 'yes/no field "#bool" is either undefined (neither True ' 'nor False) or True. More than one of is_undefined, ' 'is_false, or is_true can be set to 1. This function ' 'is usually used by the test() or is_empty() functions.') def evaluate(self, formatter, kwargs, mi, locals, field, is_undefined, is_false, is_true): # 'field' is a lookup name, not a value if field not in self.get_database(mi).field_metadata: raise ValueError(_("The column {} doesn't exist").format(field)) res = getattr(mi, field, None) if res is None: if is_undefined == '1': return 'Yes' return "" if not isinstance(res, bool): raise ValueError(_('check_yes_no requires the field be a Yes/No custom column')) if is_false == '1' and not res: return 'Yes' if is_true == '1' and res: return 'Yes' return ""

[documentos] class BuiltinRatingToStars(BuiltinFormatterFunction): name = 'rating_to_stars' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('rating_to_stars(value, use_half_stars) ' '-- Returns the rating as string of star characters. ' 'The value is a number between 0 and 5. Set use_half_stars ' 'to 1 if you want half star characters for custom ratings ' 'columns that support non-integer ratings, for example 2.5.') def evaluate(self, formatter, kwargs, mi, locals, value, use_half_stars): if not value: return '' err_msg = _('The rating must be a number between 0 and 5') try: v = float(value) * 2 except: raise ValueError(err_msg) if v < 0 or v > 10: raise ValueError(err_msg) from calibre.ebooks.metadata import rating_to_stars return rating_to_stars(v, use_half_stars == '1')

[documentos] class BuiltinSwapAroundArticles(BuiltinFormatterFunction): name = 'swap_around_articles' arg_count = 2 category = 'String manipulation' __doc__ = doc = _('swap_around_articles(val, separator) ' '-- returns the val with articles moved to the end. ' 'The value can be a list, in which case each member ' 'of the list is processed. If the value is a list then ' 'you must provide the list value separator. If no ' 'separator is provided then the value is treated as ' 'being a single value, not a list.') def evaluate(self, formatter, kwargs, mi, locals, val, separator): if not val: return '' if not separator: return title_sort(val).replace(',', ';') result = [] try: for v in [x.strip() for x in val.split(separator)]: result.append(title_sort(v).replace(',', ';')) except: traceback.print_exc() return separator.join(sorted(result, key=sort_key))

[documentos] class BuiltinArguments(BuiltinFormatterFunction): name = 'arguments' arg_count = -1 category = 'Other' __doc__ = doc = _('arguments(id[=expression] [, id[=expression]]*) ' '-- Used in a stored template to retrieve the arguments ' 'passed in the call. It both declares and initializes ' 'local variables, effectively parameters. The variables ' 'are positional; they get the value of the parameter given ' 'in the call in the same position. If the corresponding ' 'parameter is not provided in the call then arguments ' 'assigns that variable the provided default value. If ' 'there is no default value then the variable is set to ' 'the empty string.') def evaluate(self, formatter, kwargs, mi, locals, *args): # The arguments function is implemented in-line in the formatter raise NotImplementedError()

[documentos] class BuiltinGlobals(BuiltinFormatterFunction): name = 'globals' arg_count = -1 category = 'Other' __doc__ = doc = _('globals(id[=expression] [, id[=expression]]*) ' '-- Retrieves "global variables" that can be passed into ' 'the formatter. It both declares and initializes local ' 'variables with the names of the global variables passed ' 'in. If the corresponding variable is not provided in ' 'the passed-in globals then it assigns that variable the ' 'provided default value. If there is no default value ' 'then the variable is set to the empty string.') def evaluate(self, formatter, kwargs, mi, locals, *args): # The globals function is implemented in-line in the formatter raise NotImplementedError()

[documentos] class BuiltinSetGlobals(BuiltinFormatterFunction): name = 'set_globals' arg_count = -1 category = 'other' __doc__ = doc = _('set_globals(id[=expression] [, id[=expression]]*) ' '-- Sets "global variables" that can be passed into ' 'the formatter. The globals are given the name of the id ' 'passed in. The value of the id is used unless an ' 'expression is provided.') def evaluate(self, formatter, kwargs, mi, locals, *args): # The globals function is implemented in-line in the formatter raise NotImplementedError()

[documentos] class BuiltinFieldExists(BuiltinFormatterFunction): name = 'field_exists' arg_count = 1 category = 'If-then-else' __doc__ = doc = _('field_exists(field_name) -- checks if a field ' '(column) named field_name exists, returning ' "'1' if so and '' if not.") def evaluate(self, formatter, kwargs, mi, locals, field_name): if field_name.lower() in mi.all_field_keys(): return '1' return ''

[documentos] class BuiltinCharacter(BuiltinFormatterFunction): name = 'character' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('character(character_name) -- returns the ' 'character named by character_name. For example, ' r"character('newline') returns a newline character ('\n'). " "The supported character names are 'newline', 'return', " "'tab', and 'backslash'.") def evaluate(self, formatter, kwargs, mi, locals, character_name): # The globals function is implemented in-line in the formatter raise NotImplementedError()

[documentos] class BuiltinToHex(BuiltinFormatterFunction): name = 'to_hex' arg_count = 1 category = 'String manipulation' __doc__ = doc = _('to_hex(val) -- returns the string encoded in hex. ' 'This is useful when constructing calibre URLs.') def evaluate(self, formatter, kwargs, mi, locals, val): return val.encode().hex()

[documentos] class BuiltinUrlsFromIdentifiers(BuiltinFormatterFunction): name = 'urls_from_identifiers' arg_count = 2 category = 'Formatting values' __doc__ = doc = _('urls_from_identifiers(identifiers, sort_results) -- given ' 'a comma-separated list of identifiers, where an identifier ' 'is a colon-separated pair of values (name:id_value), returns a ' 'comma-separated list of HTML URLs generated from the ' 'identifiers. The list not sorted if sort_results is 0 ' '(character or number), otherwise it is sorted alphabetically ' 'by the identifier name. The URLs are generated in the same way ' 'as the built-in identifiers column when shown in Book details.') def evaluate(self, formatter, kwargs, mi, locals, identifiers, sort_results): from calibre.ebooks.metadata.sources.identify import urls_from_identifiers try: v = {} for id_ in identifiers.split(','): if id_: pair = id_.split(':', maxsplit=1) if len(pair) == 2: l = pair[0].strip() r = pair[1].strip() if l and r: v[l] = r urls = urls_from_identifiers(v, sort_results=str(sort_results) != '0') p = prepare_string_for_xml a = partial(prepare_string_for_xml, attribute=True) links = [f'<a href="{a(url)}" title="{a(id_typ)}:{a(id_val)}">{p(name)}</a>' for name, id_typ, id_val, url in urls] return ', '.join(links) except Exception as e: return str(e)

[documentos] class BuiltinBookCount(BuiltinFormatterFunction): name = 'book_count' arg_count = 2 category = 'Template database functions' __doc__ = doc = _('book_count(query, use_vl) -- returns the count of ' 'books found by searching for query. If use_vl is ' '0 (zero) then virtual libraries are ignored. This ' 'function can be used only in the GUI.') def evaluate(self, formatter, kwargs, mi, locals, query, use_vl): from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)): raise ValueError(_('The book_count() function cannot be used in a composite column')) db = self.get_database(mi) try: ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') return len(ids) except Exception: traceback.print_exc()

[documentos] class BuiltinBookValues(BuiltinFormatterFunction): name = 'book_values' arg_count = 4 category = 'Template database functions' __doc__ = doc = _('book_values(column, query, sep, use_vl) -- returns a list ' 'of the values contained in the column "column", separated ' 'by "sep", in the books found by searching for "query". ' 'If use_vl is 0 (zero) then virtual libraries are ignored. ' 'This function can be used only in the GUI.') def evaluate(self, formatter, kwargs, mi, locals, column, query, sep, use_vl): from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)): raise ValueError(_('The book_values() function cannot be used in a composite column')) db = self.get_database(mi) if column not in db.field_metadata: raise ValueError(_("The column {} doesn't exist").format(column)) try: ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0') s = set() for id_ in ids: f = db.new_api.get_proxy_metadata(id_).get(column, None) if isinstance(f, (tuple, list)): s.update(f) elif f is not None: s.add(str(f)) return sep.join(s) except Exception as e: raise ValueError(e)

class BuiltinHasExtraFiles(BuiltinFormatterFunction):
    name = 'has_extra_files'
    arg_count = -1
    category = 'Template database functions'
    __doc__ = doc = _("has_extra_files([pattern]) -- returns the count of extra "
                      "files, otherwise '' (the empty string). "
                      "If the optional parameter 'pattern' (a regular expression) "
                      "is supplied then the list is filtered to files that match "
                      "pattern before the files are counted. The pattern match is "
                      "case insensitive. "
                      'This function can be used only in the GUI.')

    def evaluate(self, formatter, kwargs, mi, locals, *args):
        if len(args) > 1:
            raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
        pattern = args[0] if len(args) == 1 else None
        db = self.get_database(mi).new_api
        try:
            files = tuple(f.relpath.partition('/')[-1] for f in
                          db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
            if pattern:
                r = re.compile(pattern, re.IGNORECASE)
                files = tuple(filter(r.search, files))
            return len(files) if len(files) > 0 else ''
        except Exception as e:
            traceback.print_exc()
            raise ValueError(e)

class BuiltinExtraFileNames(BuiltinFormatterFunction):
    name = 'extra_file_names'
    arg_count = -1
    category = 'Template database functions'
    __doc__ = doc = _("extra_file_names(sep [, pattern]) -- returns a sep-separated "
                      "list of extra files in the book's '{}/' folder. If the "
                      "optional parameter 'pattern', a regular expression, is "
                      "supplied then the list is filtered to files that match pattern. "
                      "The pattern match is case insensitive. "
                      'This function can be used only in the GUI.').format(DATA_DIR_NAME)

    def evaluate(self, formatter, kwargs, mi, locals, sep, *args):
        if len(args) > 1:
            raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
        pattern = args[0] if len(args) == 1 else None
        db = self.get_database(mi).new_api
        try:
            files = tuple(f.relpath.partition('/')[-1] for f in
                          db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
            if pattern:
                r = re.compile(pattern, re.IGNORECASE)
                files = tuple(filter(r.search, files))
            return sep.join(files)
        except Exception as e:
            traceback.print_exc()
            raise ValueError(e)

[documentos] class BuiltinExtraFileSize(BuiltinFormatterFunction): name = 'extra_file_size' arg_count = 1 category = 'Template database functions' __doc__ = doc = _("extra_file_size(file_name) -- returns the size in bytes of " "the extra file 'file_name' in the book's '{}/' folder if " "it exists, otherwise -1." 'This function can be used only in the GUI.').format(DATA_DIR_NAME) def evaluate(self, formatter, kwargs, mi, locals, file_name): db = self.get_database(mi).new_api try: q = posixpath.join(DATA_DIR_NAME, file_name) for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN): if f.relpath == q: return str(f.stat_result.st_size) return str(-1) except Exception as e: traceback.print_exc() raise ValueError(e)

class BuiltinExtraFileModtime(BuiltinFormatterFunction):
    name = 'extra_file_modtime'
    arg_count = 2
    category = 'Template database functions'
    __doc__ = doc = _("extra_file_modtime(file_name, format_string) -- returns the "
                      "modification time of the extra file 'file_name' in the "
                      "book's '{}/' folder if it exists, otherwise -1.0. The "
                      "modtime is formatted according to 'format_string' "
                      "(see format_date()). If 'format_string' is empty, returns "
                      "the modtime as the floating point number of seconds since "
                      "the epoch. The epoch is OS dependent. "
                      "This function can be used only in the GUI.").format(DATA_DIR_NAME)

    def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string):
        db = self.get_database(mi).new_api
        try:
            q = posixpath.join(DATA_DIR_NAME, file_name)
            for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
                if f.relpath == q:
                    val = f.stat_result.st_mtime
                    if format_string:
                        return format_date(datetime.fromtimestamp(val), format_string)
                    return str(val)
            return str(1.0)
        except Exception as e:
            traceback.print_exc()
            raise ValueError(e)

[documentos] class BuiltinGetNote(BuiltinFormatterFunction): name = 'get_note' arg_count = 3 category = 'Template database functions' __doc__ = doc = _("get_note(field_name, field_value, plain_text) -- fetch the " "note for field 'field_name' with value 'field_value'. If " "'plain_text' is empty, return the note's HTML. If 'plain_text' " "is non-empty, return the note's plain text. If the note " "doesn't exist, return '' in both cases. Example: " "get_note('tags', 'Fiction', '') returns the HTML of the " "note attached to the tag 'Fiction'.") def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value, plain_text): db = self.get_database(mi).new_api try: note = '' item_id = db.get_item_id(field_name, field_value) if item_id is not None: note_data = db.notes_data_for(field_name, item_id) if note_data is not None: if plain_text == '1': return note['searchable_text'].partition('\n')[2] # Return the full HTML of the note, including all images as # data: URLs. Reason: non-exported note html contains # "calres://" URLs for images. These images won't render # outside the context of the library where the note "lives". # For example, they don't work in book jackets and book # details from a different library. They also don't work in # tooltips. # This code depends on the note being wrapped in <body> tags # by parse_html. The body is changed to a <div>. That means # we often end up with <div><div> or some such, but that is # OK root = parse_html(note_data['doc']) # There should be only one <body> root = root.xpath('//body')[0] # Change the body to a div root.tag = 'div' # Expand all the resources in the note root = expand_note_resources(root, db.get_notes_resource) note = html.tostring(root, encoding='unicode') return note except Exception as e: traceback.print_exc() raise ValueError(e)

[documentos] class BuiltinHasNote(BuiltinFormatterFunction): name = 'has_note' arg_count = 2 category = 'Template database functions' __doc__ = doc = _("has_note(field_name, field_value) -- return '1' " "if the value 'field_value' in the field 'field_name' " "has an attached note, '' otherwise. Example: " "has_note('tags', 'Fiction') returns '1' if the tag " "'fiction' has an attached note, '' otherwise.") def evaluate(self, formatter, kwargs, mi, locals, field_name, field_value): db = self.get_database(mi).new_api note = None try: item_id = db.get_item_id(field_name, field_value) if item_id is not None: note = db.notes_data_for(field_name, item_id) except Exception as e: traceback.print_exc() raise ValueError(e) return '1' if note is not None else ''

_formatter_builtins = [
    BuiltinAdd(), BuiltinAnd(), BuiltinApproximateFormats(), BuiltinArguments(),
    BuiltinAssign(),
    BuiltinAuthorLinks(), BuiltinAuthorSorts(), BuiltinBookCount(),
    BuiltinBookValues(), BuiltinBooksize(),
    BuiltinCapitalize(), BuiltinCharacter(), BuiltinCheckYesNo(), BuiltinCeiling(),
    BuiltinCmp(), BuiltinConnectedDeviceName(), BuiltinConnectedDeviceUUID(), BuiltinContains(),
    BuiltinCount(), BuiltinCurrentLibraryName(), BuiltinCurrentLibraryPath(),
    BuiltinCurrentVirtualLibraryName(), BuiltinDateArithmetic(),
    BuiltinDaysBetween(), BuiltinDivide(), BuiltinEval(),
    BuiltinExtraFileNames(), BuiltinExtraFileSize(), BuiltinExtraFileModtime(),
    BuiltinFirstNonEmpty(), BuiltinField(), BuiltinFieldExists(),
    BuiltinFinishFormatting(), BuiltinFirstMatchingCmp(), BuiltinFloor(),
    BuiltinFormatDate(), BuiltinFormatDateField(), BuiltinFormatNumber(), BuiltinFormatsModtimes(),
    BuiltinFormatsPaths(), BuiltinFormatsSizes(), BuiltinFractionalPart(),
    BuiltinGetLink(),
    BuiltinGetNote(), BuiltinGlobals(), BuiltinHasCover(), BuiltinHasExtraFiles(),
    BuiltinHasNote(), BuiltinHumanReadable(), BuiltinIdentifierInList(),
    BuiltinIfempty(), BuiltinLanguageCodes(), BuiltinLanguageStrings(),
    BuiltinInList(), BuiltinIsMarked(), BuiltinListCountMatching(),
    BuiltinListDifference(), BuiltinListEquals(), BuiltinListIntersection(),
    BuiltinListitem(), BuiltinListJoin(), BuiltinListRe(),
    BuiltinListReGroup(), BuiltinListRemoveDuplicates(), BuiltinListSort(),
    BuiltinListSplit(), BuiltinListUnion(),BuiltinLookup(),
    BuiltinLowercase(), BuiltinMod(), BuiltinMultiply(), BuiltinNot(), BuiltinOndevice(),
    BuiltinOr(), BuiltinPrint(), BuiltinRatingToStars(), BuiltinRange(),
    BuiltinRawField(), BuiltinRawList(),
    BuiltinRe(), BuiltinReGroup(), BuiltinRound(), BuiltinSelect(), BuiltinSeriesSort(),
    BuiltinSetGlobals(), BuiltinShorten(), BuiltinStrcat(), BuiltinStrcatMax(),
    BuiltinStrcmp(), BuiltinStrcmpcase(), BuiltinStrInList(), BuiltinStrlen(), BuiltinSubitems(),
    BuiltinSublist(),BuiltinSubstr(), BuiltinSubtract(), BuiltinSwapAroundArticles(),
    BuiltinSwapAroundComma(), BuiltinSwitch(), BuiltinSwitchIf(),
    BuiltinTemplate(), BuiltinTest(), BuiltinTitlecase(), BuiltinToday(),
    BuiltinToHex(), BuiltinTransliterate(), BuiltinUppercase(), BuiltinUrlsFromIdentifiers(),
    BuiltinUserCategories(), BuiltinVirtualLibraries(), BuiltinAnnotationCount()
]

class FormatterUserFunction(FormatterFunction):

    def __init__(self, name, doc, arg_count, program_text, object_type):
        self.object_type = object_type
        self.name = name
        self.doc = doc
        self.arg_count = arg_count
        self.program_text = program_text
        self.cached_compiled_text = None
        # Keep this for external code compatibility. Set it to True if we have a
        # python template function, otherwise false. This might break something
        # if the code depends on stored templates being in GPM.
        self.is_python = True if object_type is StoredObjectType.PythonFunction else False

    def to_pref(self):
        return [self.name, self.doc, self.arg_count, self.program_text]

tabs = re.compile(r'^\t*')

def function_object_type(thing):
    # 'thing' can be a preference instance, program text, or an already-compiled function
    if isinstance(thing, FormatterUserFunction):
        return thing.object_type
    if isinstance(thing, list):
        text = thing[3]
    else:
        text = thing
    if text.startswith('def'):
        return StoredObjectType.PythonFunction
    if text.startswith('program'):
        return StoredObjectType.StoredGPMTemplate
    if text.startswith('python'):
        return StoredObjectType.StoredPythonTemplate
    raise ValueError('Unknown program type in formatter function pref')

def function_pref_name(pref):
    return pref[0]

def compile_user_function(name, doc, arg_count, eval_func):
    typ = function_object_type(eval_func)
    if typ is not StoredObjectType.PythonFunction:
        return FormatterUserFunction(name, doc, arg_count, eval_func, typ)

    def replace_func(mo):
        return mo.group().replace('\t', '    ')

    func = '    ' + '\n    '.join([tabs.sub(replace_func, line)
                                   for line in eval_func.splitlines()])
    prog = '''
from calibre.utils.formatter_functions import FormatterUserFunction
from calibre.utils.formatter_functions import formatter_functions
class UserFunction(FormatterUserFunction):
''' + func
    locals_ = {}
    if DEBUG and tweaks.get('enable_template_debug_printing', False):
        print(prog)
    exec(prog, locals_)
    cls = locals_['UserFunction'](name, doc, arg_count, eval_func, typ)
    return cls

def compile_user_template_functions(funcs):
    compiled_funcs = {}
    for func in funcs:
        try:
            # Force a name conflict to test the logic
            # if func[0] == 'myFunc2':
            #     func[0] = 'myFunc3'

            # Compile the function so that the tab processing is done on the
            # source. This helps ensure that if the function already is defined
            # then white space differences don't cause them to compare differently

            cls = compile_user_function(*func)
            cls.object_type = function_object_type(func)
            compiled_funcs[cls.name] = cls
        except Exception:
            try:
                func_name = func[0]
            except Exception:
                func_name = 'Unknown'
            prints('**** Compilation errors in user template function "%s" ****' % func_name)
            traceback.print_exc(limit=10)
            prints('**** End compilation errors in %s "****"' % func_name)
    return compiled_funcs

def load_user_template_functions(library_uuid, funcs, precompiled_user_functions=None):
    unload_user_template_functions(library_uuid)
    if precompiled_user_functions:
        compiled_funcs = precompiled_user_functions
    else:
        compiled_funcs = compile_user_template_functions(funcs)
    formatter_functions().register_functions(library_uuid, list(compiled_funcs.values()))

def unload_user_template_functions(library_uuid):
    formatter_functions().unregister_functions(library_uuid)

© Copyright Kovid Goyal. Última actualización el abr 19, 2024.

Table of Contents

Manual de usuario de calibre

La interfaz gráfica de usuario

Acciones

Añadir libros

Editar metadatos

Convertir libros

Mostrar

Enviar al dispositivo

Descargar noticias

Biblioteca

Dispositivo

Guardar en el disco

Conectar y compartir

Eliminar libros

Preferencias

Catálogos

Buscar y ordenar

La interfaz de búsqueda

Guardar búsquedas

Buscar en el texto completo de todos los libros

Bibliotecas virtuales

Marcar libros temporalmente

Extraer metadata de los nombres de archivo

Detalles del libro

Explorador de etiquetas

Cuadro de portadas

Explorador de portadas

Añadir notas para autores, series, etc.

Vista rápida

Tareas

Atajos de teclado

Añadir su sitio de noticias favorito

Recopilación totalmente automática

El blog de calibre

bbc.co.uk

Personalizar el proceso de obtención

Usar la versión para imprimir de bbc.co.uk

Sustituir los estilos de los artículos

Dividir y reordenar

Ejemplo de la vida real

Consejos para desarrollar nuevas fórmulas

Lecturas adicionales

Documentación de la API

Documentación de la API para fórmulas

BasicNewsRecipe

El visor de libros electrónicos

Iniciar el visor de libros

Desplazarse por un libro electrónico

Marcadores

Índice

Navegar por ubicación

Modo de referencia

Resaltar texto

Leer en alto

Buscar en el texto

Seguir enlaces usando sólo el teclado

Personalizar la apariencia de la experiencia de lectura

Consulta en diccionarios

Copiar texto e imágenes

Ampliar imágenes

Atajos de teclado

Contenido no redistribuible

Diseñar un libro para que funcione bien con el visor de calibre

Conversión de libros

Introducción

Apariencia

Tipos de letra

Texto

Distribución

Estilos

Transformar estilos

Transformar HTML

Configuración de página

Procesado heurístico

Buscar y sustituir

Detección de estructura

Capítulos y saltos de página

Miscelánea

Índice

Usar imágenes como títulos de capítulo al convertir documentos de entrada HTML

Usar atributos de etiquetas para suministrar el texto de las entradas del índice

Cómo se establecen y guardan las opciones de conversión

Consejos para formatos específicos

Convertir documentos de Microsoft Word

Archivos .doc antiguos

Convertir documentos TXT

Convertir documentos PDF

Colecciones de libros de historietas

EPUB de demostración de formato avanzado

Convertir documentos ODT

Convertir a PDF

Encabezados y pies de página

Índice imprimible

Márgenes de página específicos para archivos HTML individuales

Modificar libros electrónicos

Flujo de trabajo básico

El explorador de archivos

Cambiar nombres de archivo

Unir archivos

Cambiar el orden de los archivos de texto

Marcar la portada

Borrar archivos

Exportar archivos

Añadir nuevas imágenes, tipos de letra, etc. o crear nuevos archivos en blanco.

Sustituir archivos

Vincular hojas de estilo con archivos HTML de manera eficiente

Buscar y sustituir

Búsquedas guardadas

Modo de función

Buscar omitiendo etiquetas HTML

Herramientas automatizadas

Modificar el índice

Comprobar el libro

Añadir una portada

Incrustar tipos de letra referenciados

Reducir tipos de letra incrustados

Corregir puntuación

Transformar propiedades CSS

Eliminar reglas CSS sin usar

Corregir HTML

Embellecer archivos

Insertar un índice en línea

Establecer conceptos

Filtrar información de estilo

Actualizar el código interno del libro

Hitos

El panel de previsualización en vivo

Dividir archivos HTML

El panel de CSS en vivo

Herramientas variadas

La vista del Índice

Comprobar la ortografía en el libro

Añadir nuevos diccionarios

Insertar caracteres especiales

La vista del inspector de código

Comprobar enlaces externos

Descargar recursos externos

Organizar archivos en carpetas por tipo

Importar archivos en otros formatos de libro electrónico como EPUB

La herramienta de Informes

Características especiales del editor de código

Resaltado de sintaxis

Ayuda sensible al contexto

Completado automático

Fragmentos

El servidor de contenidos de calibre

Acceder al servidor de contenidos desde otros dispositivos

Acceder al servidor desde dispositivos en la red local

Resolución de problemas de conexión en la red local

Acceder al servidor desde cualquier parte en Internet

La interfaz del servidor

La lista de libros

El lector de libros

Soporte de navegadores

Activar modo sin conexión

Administrar cuentas de usuario en la línea de órdenes

Integrar el servidor de contenidos de calibre en otros servidores

Usar un servidor virtual completo

Usar un prefijo de URL

Crear un servicio para el servidor de calibre en un sistema Linux moderno

Comparar libros electrónicos

Comprender la vista de comparación

Iniciar la herramienta de comparación

Comparar dos archivos de libro electrónico

Comparar el ORIGINAL_FMT con FMT

Comparar un hito con el estado actual del libro durante su modificación

Modificar los metadatos de los libros

Editar metadatos de libros, de uno en uno

Descargar metadatos

Gestionar formatos de libros

Todo sobre las portadas

Modificar los metadatos de varios libros a la vez

Buscar y sustituir

Descargar metadatos en masa

Añadir archivos de datos adicionales a un libro

Preguntas frecuentes

Conversión de formatos de libro electrónico

¿Qué formatos puede convertir calibre?

¿Cuáles son los mejores formatos de origen para convertir?.

He convertido un archivo PDF, pero el resultado tiene varios problemas.

¿Cómo convierto un archivo que contiene caracteres «exóticos» (acentuados, no latinos, comillas tipográficas, etc.)?

¿Qué ocurre con los índices de los archivos MOBI?

¿Cómo convierto una colección de archivos HTML en un orden específico?

El EPUB que he creado con calibre no es válido

¿Cómo puedo utilizar algunas de las funciones avanzadas de las herramientas de conversión?

Integración con dispositivos

¿Cuáles dispositivos admite calibre?

¿Cómo puedo ayudar para que mi dispositivo sea compatible con calibre?

Mi dispositivo no es detectado por calibre

Mi dispositivo no es estándar o es inusual. ¿Qué puedo hacer para conectar con él?

¿Cómo puedo usar calibre con mi iPad/iPhone/iPod touch?

Usar el servidor de contenidos

¿Cómo utilizo calibre con mi teléfono o tableta Android o mi Kindle Fire?

Con un cable USB

Por la red

¿Puedo acceder a mis libros en calibre utilizando el navegador de Internet de mi Kindle u otro dispositivo de lectura?

No puedo enviar correos electrónicos usando calibre

Mi dispositivo está montado en Linux como sólo lectura, por lo que calibre no puede conectarse con él.

¿Por qué calibre no es compatible con las colecciones de Kindle o los estantes de Nook?

Me sale un error cuando trato de usar calibre con el Kobo Touch, Glo, etc.

¿Por qué las portadas que envío al Kindle de tinta electrónica aparecen un momento y luego son sustituidas por una portada genérica?

Las portadas de los archivos MOBI han dejado de aparecer en Kindle para PC, Kindle para Android, iPad, etc.

He enviado algunos libros al Kindle con calibre y no aparecen

Gestión de la biblioteca

¿Dónde se almacenan los archivos de los libros?

¿Cómo gestiona calibre los nombres de autor y su orden?

¿Por qué calibre no me deja guardar mis libros en mi propia estructura de carpetas?

¿Por qué calibre no tiene una columna para tal o cual cosa?

¿Puedo tener una columna que muestre los formatos o el ISBN?

¿Cómo muevo mis datos de calibre de un equipo a otro?

¡La lista de libros en calibre está vacía!

Estoy sufriendo errores con la biblioteca calibre en una unidad de red o NAS

Miscelánea

¿Amazon va a interrumpir el envío por correo electrónico de archivos MOBI?

Quiero que calibre descargue noticias de mi sitio de noticias favorito.

¿Por qué el programa se llama «calibre»?

¿Por qué calibre no muestra todos los tipos de letra de macOS?

calibre no se inicia en Windows

calibre se bloquea o cierra ocasionalmente

Ni el visor ni la herramienta de modificación de libros electrónicos de calibre funcionan e Windows

Al usar el visor o realizar una conversión aparece un error de permiso denegado en Windows.

calibre no se inicia o se detiene en macOS

Sólo veo una pantalla negra o blanca al ejecutar el visor de libros electrónicos de calibre

He descargado el instalador, pero no funciona.

Mi programa antivirus dice que calibre es un virus o un troyano

¿Cómo hago copias de respaldo de calibre?

¿Cómo uso en calibre los libros EPUB comprados (o qué hago con los archivos .acsm)?

Me sale un error de «Permiso denegado»

¿Puedo hacer que los metadatos de comentario se muestren en el lector?

¿Cómo hago que calibre utilice mi proxy HTTP?

Quiero que se añada una función a calibre. ¿Qué puedo hacer?

¿Por qué calibre no se actualiza automáticamente?

¿Qué licencia tiene calibre?

¿Cómo ejecuto calibre desde una memoria USB?

¿Cómo ejecuto partes de calibre como la descarga de noticias o el servidor de contenido en mi propio servidor Linux?

Cursillos

Añadir su sitio de noticias favorito

Recopilación totalmente automática

El blog de calibre

bbc.co.uk

Personalizar el proceso de obtención

Usar la versión para imprimir de bbc.co.uk

Sustituir los estilos de los artículos

Dividir y reordenar

Ejemplo de la vida real

Consejos para desarrollar nuevas fórmulas

Lecturas adicionales

Documentación de la API

Documentación de la API para fórmulas

Gestionar subgrupos de libros, por ejemplo «género»

Configuración

Búsqueda

Restricciones

Funciones de plantilla útiles

Cursillo de XPath

Seleccionar por nombre de etiqueta

Seleccionar por atributos

Seleccionar por contenido de etiqueta

Libro de muestra

Funciones XPath predefinidas

El lenguaje de plantillas de calibre

Plantillas básicas

Formato avanzado

Usar plantillas para definir columnas personalizadas

Plantillas y controles de metadatos

Usar funciones en plantillas: modo de función única

Modo de programación general

Programas más complejos en expresiones de plantilla: modo de programación de plantillas

Modo de plantilla de Python

Plantillas guardadas

Pasar información adicional a las plantillas

Notas sobre las diferencias entre modos

Funciones de plantilla Python definidas por el usuario

Notas especiales para plantillas de guardado o envío

Consejos

Referencia de funciones

Reference for all built-in template language functions

Todo acerca de cómo utilizar expresiones regulares en calibre

Primero, una advertencia y unas palabras de aliento

¿Dónde puedo utilizar expresiones regulares en calibre?

¿Pero qué es una expresión regular?

¿Me lo puede explicar?

No suena tan mal. ¿Qué viene después?

¡Estupendo! ¡Esto empieza a tener sentido!

Bien, estos caracteres especiales están muy bien, pero ¿qué ocurre si quiero hacer coincidir un punto o un signo de interrogación?

¿Y cuáles son los conjuntos más útiles?

Pero si tengo varios textos diferentes para hacer coincidir, ¿las cosas se complican?

Falta algo…

¿No dijo al principio que hay una manera de hacer que una expresión regular no distinga entre mayúsculas y minúsculas?

Creo que empiezo a entender esto de las expresiones regulares… ¿cómo las utilizo en calibre?

Conversiones

Añadir libros

Modificar metadatos en masa

Referencia rápida

Referencia rápida para la sintaxis de expresiones regulares

Créditos

Escribir sus propios complementos para extender la funcionalidad de calibre

Anatomía de un complemento de calibre

Un complemento de interfaz de usuario

__init__.py

ui.py

main.py

Obtener recursos del archivo ZIP del complemento

Habilitar la configuración de usuario para el complemento

Complementos para modificar libros

main.py

Añadir traducciones al complemento

La API del complemento

Depurar complementos

Más ejemplos de complementos

Compartir sus complementos con otros

Escribir fórmulas matemáticas en libros electrónicos

Un archivo HTML sencillo con fórmulas

Más información

Crear catálogos AZW3 • EPUB • MOBI

Seleccionar libros para catalogar

Secciones incluidas

Prefijos

Libros excluidos

Géneros excluidos

Otras opciones

Portadas personalizadas para catálogos

Otras fuentes de ayuda

Bibliotecas virtuales

Crear bibliotecas virtuales

Ejemplos de bibliotecas virtuales útiles

Trabajar con bibliotecas virtuales

Usar bibliotecas virtuales en búsquedas

Usar restricciones adicionales

El esquema de URL calibre://

Cambiar a una biblioteca específica

Mostrar un libro específico en calibre

Abrir un libro específico en el visor de libros electrónicos en una posición específica

Buscar libros

Abrir la ventana de detalles de un libro en alguna biblioteca

Abrir las notas asociadas con un autor, serie, etc.

Codificación hexadecimal de parámetros de URL

Personalizar calibre

Variables de entorno

Ajustes

Reemplazar iconos, plantillas, etcétera

Crear un tema de iconos propio para calibre

Personalizar calibre con complementos

Interfaz de línea de órdenes

Órdenes con documentación

calibre

[opciones]

calibre-customize

[opciones]

calibre-debug

[opciones]

calibre-server

[opciones]

calibre-smtp

[opciones]

calibredb

OPCIONES GLOBALES

list

add

remove

add_format

remove_format

show_metadata

set_metadata

export

catalog

saved_searches

add_custom_column

custom_columns

remove_custom_column

set_custom

restore_database

check_library

list_categories

backup_metadata

clone

embed_metadata

search

fts_index

fts_search

ebook-convert

OPCIONES DE ENTRADA

OPCIONES DE SALIDA

APARIENCIA

PROCESADO HEURÍSTICO

BUSCAR Y SUSTITUIR

DETECCIÓN DE ESTRUCTURA

ÍNDICE

METADATOS

DEPURACIÓN

ebook-edit

[opciones]

ebook-meta

[opciones]

ebook-polish

[opciones]

ebook-viewer

[opciones]

fetch-ebook-metadata

[opciones]

lrf2lrs

[opciones]

lrfviewer

[opciones]

lrs2lrf

[opciones]

web2disk

[opciones]

Órdenes sin documentación

Configurar un entorno de desarrollo de calibre

Filosofía del diseño

Estructura del código

Obtener el código

Enviar cambios para que se incluyan

Entorno de desarrollo en Windows

Entorno de desarrollo macOS

Entorno de desarrollo Linux

Mantener una instalación «normal» y otra de «desarrollo» de calibre en el mismo equipo

Consejos de depuración

Usar sentencias de impresión

Usar un intérprete de Python interactivo

Usar el depurador de Python como un depurador remoto

Usar el depurador en su IDE de Python favorito

Ejecutar scripts arbitrarios en el entorno Python de calibre

Usar calibre en sus proyectos

Instalación binaria de calibre

Instalación de código fuente sobre Linux

Documentación de la API de varias partes de calibre

Documentación de la API para fórmulas

BasicNewsRecipe

Documentación de la API para complementos

Complemento

FileTypePlugin

Complementos de metadatos

Complementos de catálogo

Complementos de descarga de metadatos

Complementos de conversión

Controladores de dispositivo

Acciones de interfaz de usuario

Complementos de preferencias

Documentación de la API para la interfaz de la base de datos

Cache

Documentación de la API para las herramientas de modificación de libros electrónicos

El objeto contenedor

Gestionar los archivos dentro de un contenedor

Presentación mejorada y corrección automática de errores

Gestionar las sobrecubiertas de los libros

Dividir y combinar archivos

Gestionar portadas

Trabajar con CSS

Trabajar con el índice

Herramienta para modificar libro

Controlar la interfaz de usuario del editor

Gestión de derechos digitales (DRM)

¿Qué implica DRM para mí personalmente?

¿Qué hace DRM por los autores?

DRM y libertad

¿Por que calibre no admite DRM?

¿Cual es la posición de calibre respecto a los proveedores de contenido?

¿Como puedo ayudar a combatir el DRM?

Glosario