calibre
Author: Kovid Goyal
Date: 2024-05-03T03:02:27+00:00
Description: Comprehensive documentation for calibre

Navigation

- modules
- suivant |
- Démarrer »
- Manuel de l’Utilisateur calibre

Manuel de l’Utilisateur calibre
calibre est un gestionnaire de bibliothèque de livres numériques. Il peut visualiser, convertir et cataloguer des livres numériques dans la plupart des principaux formats. Il peut aussi communiquer avec beaucoup de périphériques de lecture de livre numérique. Il peut aller sur Internet et chercher des métadonnées pour vos livres. Il peut télécharger des journaux et les convertir en livres numériques pour une lecture confortable. Il est multi-plateforme, fonctionnant sur Linux, Windows et Mac macOS .

Vous venez de commencer avec calibre. Qu’allez-vous faire maintenant ? Avant que calibre puisse faire quoique ce soit avec vos livres numériques, il faut d’abord qu’il les connaisse. Glissez et déposez quelques fichiers de livre numérique dans calibre, ou cliquez sur le bouton « Ajouter des livres » et sélectionnez les livres numériques avec lesquels vous voulez travailler. Une fois que vous aurez ajouté ces livres, ils apparaîtront dans l’écran principal ressemblant à quelque chose comme ceci :

Une fois que vous aurez admiré avec satisfaction la liste des livres que vous venez d’ajouter, vous aurez probablement envie de les lire. Pour ce faire, vous devrez convertir le livre dans un format que votre liseuse comprenne. Lors de la première ouverture de calibre, l’Assistant de bienvenue a démarré et a paramétré calibre pour votre périphérique de lecture. La conversion est un jeu d’enfant. Il suffit de sélectionner le livre que vous voulez convertir, puis de cliquer sur le bouton « Convertir des livres ». Ignorer toutes les options pour l’instant et cliquez sur « OK ». La petite icône dans le coin en bas à droite va commencer à tourner. Une fois que c’est fini, votre livre converti est prêt. Cliquez sur le bouton « Visualiser » pour lire le livre.

Si vous voulez lire le livre sur votre liseuse, connecter la liseuse à l’ordinateur, attendez que calibre la détecte (10-20 secondes), puis cliquez sur le bouton « Envoyer au périphérique ». Une fois que l’icône s’arrête de tourner à nouveau, déconnectez votre liseuse et lisez tout de suite ! Si vous n’aviez pas converti le livre dans l’étape précédente, calibre l’aura automatiquement fait pour que le livre soit dans un format que votre périphérique de lecture peut comprendre.

Pour vous lancer dans un usage plus avancé, vous devriez lire L’Interface Graphique Utilisateur. Pour encore plus de puissance et de polyvalence, apprenez Interface en ligne de commande. Vous trouverez la liste des Questions Fréquemment Posées bien utile elle aussi.

Si vous avez d’autres questions ou si vous voulez discuter de calibre avec d’autres utilisateurs, demander de l’aide pour des choses spécifiques, il existe des forums et d’autres ressources d’aide disponibles [https://calibre-ebook.com/fr/help].

Sections

- L’Interface Graphique Utilisateur
- Ajouter votre site web favori d’actualités
- La visionneuse de livre numérique
- Conversion de livre numérique
- Éditer des livres numériques
- Le Serveur de contenu calibre
- Comparer des livres numériques
- Éditer les métadonnées d’un livre numérique
- Questions Fréquemment Posées
- Tutoriels
- Le système d’URL calibre://
- Personnaliser calibre
- Interface en ligne de commande

- Mise en place d’un environnement de développement calibre

- Gestion des Droits Numériques (GDN - DRM)

- Glossaire

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- L’Interface Graphique Utilisateur

L’Interface Graphique Utilisateur

L’Interface Graphique Utilisateur (GUI) permet d’accéder à toute la gestion de la bibliothèque et aux caractéristiques de conversion des formats de livre numérique. La méthode de travail basique pour l’utilisation de calibre est d’abord d’ajouter des livres à la bibliothèque depuis votre disque dur. calibre essaiera de lire les métadonnées des livres et les ajoutera à sa base de donnée interne. Une fois qu’ils sont dans la base de données, vous pouvez effectuer différentes Actions sur ceux-ci incluant la conversion d’un format à un autre, transférer vers le périphérique de lecture, visualiser sur votre ordinateur, et éditer les métadonnées. Cette dernière inclut la modification de la couverture, de la description et les étiquettes parmi d’autres détails. Notez que calibre crée des copies des fichiers que vous lui ajoutez. Vos fichiers originaux sont laissés intacts.

L’interface est divisée en différentes sections :

- Actions

- Préférences

- Catalogues

- Rechercher et trier

- L’interface de recherche

- Recherches sauvegardées

- Recherche dans le texte intégral de tous les livres

- Bibliothèques virtuelles

- Marquage temporaire des livres

- Deviner les métadonnées à partir des noms de fichier

- Détails du livre

- Navigateur d’étiquettes

- Grille de couvertures

- Navigateur de couvertures

- Ajouter des notes pour les auteurs, les séries, etc.

- Affichage rapide

- Tâches

- Raccourcis clavier

Actions

La barre d’outils contient des raccourcis pratiques pour les actions couramment utilisées. Si vous faites un clic droit sur les icônes, vous pouvez effectuer des variations de l’action par défaut. Notez que la barre d’outils a une apparence légèrement différente selon que vous avez un lecteur de livre numérique connecté à votre ordinateur ou pas.

- Ajouter des livres

- Modifier les métadonnées

- Convertir des livres

- Visualiser

- Envoyer vers le périphérique

- Récupérer des informations

- Bibliothèque

- Périphérique connecté

- Enregistrer sous…

- Connecter/partager

- Supprimer des livres

Ajouter des livres

L’action Ajouter des livres a sept variations accessibles en faisant un clic droit sur le bouton.

1. Ajouter des livres à partir d’un dossier unique : ouvre une boîte de dialogue de sélection de fichiers et vous permet de spécifier quels livres doivent être ajoutés à partir d’un dossier. Cette action est sensible au contexte, c’est à dire qu’elle dépend de quel catalogue vous avez sélectionné. Si vous avez sélectionné la Bibliothèque , les livres seront ajoutés à la bibliothèque. Si vous avez sélectionné le périphérique de lecture de livre numérique, les livres seront téléchargés sur le périphérique, et ainsi de suite.

2. Ajouter les livres à partir des dossiers et sous-dossiers : Permet de choisir un dossier. Le dossier et tous ses sous-dossiers sont scannés récursivement, et les livres numériques trouvés sont ajoutés à la bibliothèque. Vous pouvez choisir de faire ajouter par calibre tous les fichiers présents dans un seul dossier à une seule fiche de livre ou à plusieurs fiches de livre. calibre suppose que chaque dossier contient un seul livre. Tous les fichiers de livres numériques d’un dossier sont supposés être le même livre dans différents formats. Cette action est l’inverse de l’action Enregistrer sous…`<save_to_disk_multiple>, c’est-à-dire que vous pouvez :guilabel:`Enregistrer sous…, supprimer les livres et les rajouter en mode livre unique par dossier, sans aucune information perdue à l’exception de la date (cela suppose que vous n’avez pas modifié les paramètres de l’action Enregistrer sous…).

3. Ajouter de multiples livres à partir d’un dossier compressé (ZIP/RAR) : vous permet d’ajouter de multiples livres numériques qui sont stockés à l’intérieur des fichiers ZIP ou RAR sélectionnés. C’est un raccourci pratique qui évite de devoir d’abord décompresser des archives et puis d’ajouter les livres par l’intermédiaire d’une des deux options ci-dessus.

4. Ajouter un livre vide (une entrée de livre sans formats) : vous permet de créer un fichier livre vide. Cela peut servir à remplir manuellement les informations sur un livre que vous n’avez pas encore dans votre collection.

5. Ajouter à partir de l’ISBN : Vous permet d’ajouter un ou plusieurs livres en entrant leurs numéros ISBN.

6. Ajouter les fichiers aux enregistrements de livre sélectionnés : Vous permet d’ajouter ou de mettre les fichiers à jour liés à un livre existant dans votre bibliothèque.

7. Ajouter des fichiers de données aux enregistrements de livres sélectionnés : Permet d’ajouter un nombre quelconque de fichiers supplémentaires qui seront stockés dans un sous-répertoire data dans le répertoire du livre. Voir Ajouter des fichiers de données supplémentaires à un livre pour plus de détails.

8. Ajouter un fichier vide aux fiches de livres sélectionnées : Vous permet d’ajouter un fichier vide du format spécifié aux fiches de livres sélectionnées.

L’action Ajouter des livres peut lire les métadonnées à partir d’une grande variété de formats de livres numériques. En outre, calibre essaie de deviner les métadonnées du fichier. Voir la section Deviner les métadonnées à partir des noms de fichier pour apprendre comment configurer cela.

Pour ajouter un format additionnel à un livre existant vous pouvez faire n’importe laquelle de ces trois actions :

1. Glisser et déposer le fichier sur le panneau des détails du livre sur le côté droit de la fenêtre principale.

2. Faire un clic droit sur le bouton Ajouter des livres et choisir Ajouter des fichiers aux livres sélectionnés.

3. Cliquer le bouton Ajouter des livres dans la partie supérieure droite de la boîte de dialogue Modifier les métadonnées, accédé par l’action Modifier les métadonnées.

Modifier les métadonnées

L’action Modifier les métadonnées a quatre variations accessibles en faisant un clic droit sur le bouton.

1. Modifier les métadonnées individuellement : permet de modifier les métadonnées des livres un par un, avec la possibilité de récupérer des métadonnées, y compris les couvertures, à partir de l’Internet. Il vous permet également d’ajouter ou de supprimer des formats de livres numériques particuliers pour ce livre.

2. Modifier les métadonnées par lot : Permet de modifier les champs de métadonnées communes à plusieurs livres, de manière simultanée. Cela fonctionne pour tous les livres que vous avez sélectionnés dans la Vue bibliothèque.

3. Télécharger les métadonnées et les couvertures : Télécharge les métadonnées et les couvertures (si disponible) pour les livres qui sont sélectionnés dans la liste de livres.

4. Fusionner les enregistrements de livres : Vous donne la possibilité de fusionner les métadonnées et les formats de deux ou plusieurs enregistrements de livres. Vous pouvez choisir de supprimer ou de conserver les enregistrements qui n’ont pas été cliqué en premier.

5. Gérer les fichiers de données : Gérer les fichiers de données supplémentaires associés aux livres sélectionnés.

Pour plus de détails, voir Éditer les métadonnées d’un livre numérique.

Convertir des livres

Les livres numériques peuvent être convertis à partir d’un certain nombre de formats dans le format que votre liseuse préfère. De nombreux livres numériques disponibles à l’achat seront protégés par la technologie Digital Rights Management (DRM). calibre ne sait pas convertir ces livres numériques. Il est facile de supprimer les DRM de nombreux formats, mais c’est illégal, vous devrez trouver vous-même des outils pour déverrouiller vos livres et utiliser ensuite calibre pour les convertir.

Pour la plupart des gens, la conversion devrait être l’affaire d’un simple clic. Si vous voulez en savoir plus sur le processus de conversion, voir Conversion de livre numérique.

L’action Convertir des livres a trois variations accessibles en faisant un clic droit sur le bouton.

1. Convertir individuellement : Vous permet de spécifier les options de conversion pour personnaliser la conversion de chaque livre numérique sélectionné.

2. Convertir par lot : Vous permet de spécifier des options qu’une seule fois pour convertir un certain nombre de livres numériques en même temps.

3. Créer un catalogue des livres de votre bibliothèque calibre : Vous permet de générer une liste complète des livres dans votre bibliothèque, en y précisant les métadonnées, et ce dans plusieurs formats tels que XML, CSV, BiBTeX, EPUB et MOBI. Le catalogue contiendra tous les livres se trouvant visibles à ce moment dans la vue bibliothèque. Cela vous permet d’utiliser les fonctions de recherche pour limiter les livres à cataloguer. En outre, si vous sélectionnez plusieurs livres avec la souris, seuls les livres sélectionnés seront compris dans le catalogue. Si vous générez le catalogue dans un format de livre numérique comme EPUB, MOBI ou AZW3, la prochaine fois que vous connecterez votre lecteur de livre numérique, le catalogue sera envoyé automatiquement au périphérique. Pour avoir plus d’informations sur comment fonctionne les catalogues lisez Création de catalogues AZW3 • EPUB • MOBI.

Visualiser

L’action Visualiser affiche le livre dans un programme de visualisation de livre numérique. calibre a une visionneuse intégrée qui fonctionne pour de nombreux formats de livre numérique. Pour d’autres formats, il utilise l’application du système d’exploitation par défaut. Vous pouvez configurer les formats qui doivent s’ouvrir avec la visionneuse interne via Préférences → Intervace → Comportement. Si un livre a plus d’un format, vous pouvez voir un format particulier en faisant un clic droit sur le bouton.

Envoyer vers le périphérique

L’action Envoyer vers le périphérique dispose de huit variations, accessibles en faisant un clic droit sur le bouton.

1. Envoyer vers la mémoire principale : Les livres sélectionnés sont transférés vers la mémoire principale du lecteur de livre numérique.

2. Envoyer vers la carte mémoire (A) : Les livres sélectionnés sont transférés vers la carte de stockage (A) du lecteur de livre numérique.

3. Envoyer vers la carte mémoire (B) : Les livres sélectionnés sont transférés vers la carte de stockage (B) du lecteur de livre numérique.

4. Envoyer un format spécifique vers : Les livres sélectionnés sont transférés vers l’emplacement de stockage sélectionné du périphérique, dans le format que vous spécifiez.

5. Éjecter le périphérique : Détache le périphérique de calibre.

6. **Indiquer l’action par défaut pour envoyer au dispoitif ** : Vous permet de spécifier laquelle des options, 1 à 5 ci-dessus ou 7 ci-dessous, sera l’action par défaut lorsque vous cliquez sur le bouton principal.

7. Envoyer et effacer de la bibliothèque : Les livres sélectionnés sont transférés vers l’emplacement de stockage sélectionné sur le périphérique, et puis supprimées de la bibliothèque.

8. Récupérer les annotations (expérimental) : Transfère des annotations que vous avez faites sur un livre numérique depuis votre périphérique, vers la métadonnée commentaires du livre dans la bibliothèque calibre.

Vous pouvez contrôler le nom du fichier et la structure du dossier des fichiers envoyés au périphérique en mettant en place un modèle dans Préférences → Importer/exporter → Envoyer des livres aux périphériques. Voir également Le langage de modèle calibre.

Récupérer des informations

L’action Récupérer des Actualités permet de télécharger des actualités de divers sites web et de les convertir en un livre numérique qui peut être lu sur votre liseuse. Normalement, le livre numérique nouvellement créé est ajouté à votre bibliothèque, mais si un lecteur de livre numérique est connecté au moment où se termine le téléchargement, les actualités sont également automatiquement téléchargées sur le lecteur.

L’action Récupérer des Actualités utilise une simple recette (10-15 lignes de code) pour chaque site d’actualité. Pour apprendre à créer des recettes pour vos propres sources d’informations, voir Ajouter votre site web favori d’actualités.

L’action Récupérer des informations a trois variations accessibles en faisant un clic droit sur le bouton.

1. Planifier le téléchargement des informations : Permet de programmer le téléchargement de vos sources d’actualités sélectionnées à partir d’une liste de centaines disponibles. La planification peut être réglée de manière particulière pour chaque source d’actualités que vous sélectionnez et la planification est flexible vous permettant de sélectionner certains jours de la semaine ou une fréquence de jours entre les téléchargements.

2. Ajouter une source personnalisée d’actualités : Vous permet de créer une recette simple pour télécharger des nouvelles d’un site d’information personnalisé auquel vous souhaitez accéder. La création de la recette peut être aussi simple que de spécifier un flux d’actualités RSS, ou vous pouvez être plus normatif en créant un code sur base Python pour la tâche. Pour plus d’information voir Ajouter votre site web favori d’actualités.

3. Télécharger toutes les sources d’actualités planifiées : entraîne que calibre se met immédiatement à télécharger toutes les sources d’information que vous avez programmées.

Bibliothèque

L’action Bibliothèque vous permet de créer, de basculer entre, de renommer ou de supprimer une bibliothèque. calibre vous permet de créer autant de bibliothèques que vous le souhaitez. Vous pourriez par exemple, créer une bibliothèque fiction, une bibliothèque non-fiction, une bibliothèque de langue étrangère, une bibliothèque projet, ou toute structure qui conviendrait à vos besoins. Les bibliothèques sont les plus hautes structures organisationnelles dans calibre. Chaque bibliothèque a son propre ensemble de livres, étiquettes, catégories et emplacement de stockage de base.

1. Basculer/créer une bibliothèque…. Vous permet de ; a) vous connecter à une bibliothèque calibre préexistante à un autre emplacement, b) créer une bibliothèque vide à un nouvel emplacement ou, c) déplacer la bibliothèque existante à un emplacement nouvellement spécifié.

2. Bascule rapide : Vous permet de basculer entre des bibliothèques qui ont été enregistrée ou créée dans calibre.

3. Renommer un bibliothèque : Vous permet de renommer une Bibliothèque.

4. Lire un livre au hasard : Choisit un livre au hasard dans la bibliothèque pour vous

5. Supprimer une bibliothèque : Vous permet de dés-enregistrer une bibliothèque de calibre.

6. Exporter/importer toutes les données de calibre : Vous permet soit d’exporter des données de calibre pour la migration vers un nouvel ordinateur, soit d’importer des données exportées précédemment.

7. <library name> : Les actions 7, 8, etc…. vous permettent de basculer immédiatement entre plusieurs bibliothèques que vous avez créées ou auxquelles vous vous êtes connecté. Cette liste ne contient que les 5 bibliothèques les plus utilisées. Pour la liste complète, utilisez le menu Changement rapide.

8. Maintenance de la bibliothèque : Vous permet de vérifier les problèmes de consistance de la bibliothèque courante et restaure la base de donnée de la bibliothèque depuis des sauvegardes.

Note

Les métadonnées concernant vos livres numériques, par ex. titre, auteur, et étiquettes sont stockées dans un seul fichier dans votre répertoire de bibliothèque calibre appelé metadata.db. Si ce fichier se corrompt (un événement très rare), vous perdez les métadonnées. Heureusement, calibre sauvegarde automatiquement les métadonnées pour chaque livre individuel dans le répertoire du livre en tant que fichier OPF. En utilisant l’action Restaurer la base de données sous Maintenance de la bibliothèque décrite ci-dessus, calibre peut reconstruire le fichier metadata.db depuis les fichiers individuels OPF pour vous.

Vous pouvez copier ou déplacer des livres entre les différentes bibliothèques (une fois que vous avez installé plus d’une bibliothèque) en faisant un clic-droit sur le livre et en sélectionnant l’action Copier dans la bibliothèque.

Périphérique connecté

L’action Périphérique vous permet de consulter les livres de la mémoire principale ou des cartes de stockage de votre périphérique de lecture, ou d’éjecter le périphérique (le détacher de calibre). Cette icône apparaît automatiquement dans la barre d’outils principale de calibre lorsque vous connectez un périphérique pris en charge. Vous pouvez cliquer sur l’icône pour voir les livres qui sont sur votre périphérique. Vous pouvez également faire glisser et déposer des livres de votre bibliothèque calibre sur l’icône Périphérique pour les transférer sur votre liseuse. Inversement, vous pouvez glisser et déposer des livres à partir de votre périphérique sur l’icône de la bibliothèque de la barre d’outils pour transférer des livres à partir de votre périphérique vers la bibliothèque calibre.

Enregistrer sous…

L’action Enregistrer sous… a cinq variations, accessibles en faisant un clic droit sur le bouton.

1. Enregistrer sous… : sauve les livres sélectionnés vers le disque organisés en dossiers. La structure du dossier ressemble à

Author_(sort)
Title
Book Files

Vous pouvez contrôler le nom du fichier et la structure des répertoires des fichiers lors de la copie sur le disque en mettant en place un modèle dans Préférences → Importer/exporter → Sauvegarde de livres sur le disque. Voir aussi Le langage de modèle calibre.

2. Enregistrer sur le disque dans un seul dossier : Enregistre les livres sélectionnés vers le disque dans un dossier unique.

Pour 1. et 2., tous les formats disponibles, ainsi que les métadonnées, sont stockés sur le disque pour chaque livre sélectionné. Les métadonnées sont stockées dans un fichier OPF. Livres enregistrés peuvent être réimportés dans la bibliothèque sans aucune perte d’information en utilisant l’action Ajouter des livres.

3. Enregistrer seulement le format *<votre format préféré>* sur le disque : Enregistre les livres sélectionnés sur le disque dans la structure de dossier comme montrée en (1), mais seulement pour votre format de livre numérique préféré. Vous pouvez définir votre format préféré dans Préférences → Interface → Comportement → Format de sortie préféré.

4. Enregistrer seulement le format *<votre format préféré>* sur le disque dans un dossier unique : Enregistre les livres sélectionnés sur le disque dans un dossier unique, mais seulement dans votre format de livre numérique préféré. Vous pouvez définir votre format préféré dans Préférences → Interface → Comportement → Format de sortie préféré.

5. Enregistrer un seul format vers le disque… : Enregistre les livres sélectionnés sur le disque dans la structure de dossier comme montrée en (1), mais seulement dans le format que vous sélectionnez dans la liste contextuelle.

Connecter/partager

L’action Connecter/partager vous permet de vous connecter manuellement à un périphérique ou un dossier sur votre ordinateur. Il vous permet également de mettre en place l’accès à votre bibliothèque calibre via un navigateur web ou par courriel.

L’action Connecter/partager a quatre variations, accessibles en faisant un clic-droit sur le bouton.

1. Connectez au dossier : Vous permet de vous connecter à n’importe quel dossier de votre ordinateur comme s’il s’agissait d’un périphérique et d’utiliser pour ce dossier toutes les fonctionnalités que calibre utilise avec les périphériques. Utile si votre périphérique ne peut pas être pris en charge par calibre, mais est disponible en tant que disque USB.

2. Démarrer le Serveur de contenu : Démarre le serveur web intégré de calibre. Au démarrage, votre bibliothèque calibre sera accessible via un navigateur Web à partir d’Internet (si vous le voulez). Vous pouvez configurer la façon dont le serveur Web est accessible en définissant des préférences dans Préférences → Partager → Partager via Internet

3. Configurer le partage des livres par courriel : Permet de partager des livres et des fils d’information par courriel. Après la mise en place des adresses courriel pour cette option, calibre enverra les mises à jour des actualités et les mises à jour de livres au carnet d’adresses de messagerie saisies. Vous pouvez configurer la façon dont calibre envoie un courriel en définissant des préférences dans Préférences →  Partager →  Partager des livres par courriel. Une fois que vous avez mis en place une ou plusieurs adresses courriel, cette entrée de menu sera remplacée par des entrées de menu pour envoyer des livres aux adresses de messagerie configurées.

Supprimer des livres

L’action Supprimer des livres supprime les livres de manière permanente aussi utilisez-la avec précaution. Il est sensible au contexte, c’est à dire qu’il dépend de quel catalogue vous avez sélectionné. Si vous avez sélectionné Bibliothèque, les livres seront retirés de la bibliothèque. Si vous êtes sur la liste des livres de votre liseuse, les livres seront retirés du périphérique. Pour supprimer uniquement un format particulier pour un livre donné, utiliser l’action Modifier les métadonnées. Supprimer des livres a également cinq variations qui peuvent être accessibles en faisant un clic-droit sur le bouton.

1. Supprimer les livres sélectionnés : Vous permet de supprimer définitivement tous les livres qui sont sélectionnés dans la liste de livres.

2. Supprimer les fichiers d’un format spécifique pour les livres sélectionnés … : Permet de supprimer définitivement un fichier de livre numérique d’un format spécifié pour ceux qui sont sélectionnés dans la liste de livres.

3. Supprimer tous les formats pour les livres sélectionnés, excepté… : Vous permet de supprimer définitivement les fichiers de livres numériques de n’importe quel format sauf celui spécifié pour ceux qui sont sélectionnés dans la liste de livres.

4. Supprimer tous les formats des livres sélectionnés : Vous permet de supprimer définitivement tous les formats pour les livres qui sont sélectionnés dans la liste de livres. Seules les métadonnées seront conservées.

5. Supprimer les couvertures des livres sélectionnés : Vous permet de supprimer définitivement des fichiers de couverture des livres qui sont sélectionnés dans la liste de livres.

6. Supprimer les livres correspondants du périphérique : Vous permet de supprimer les livres numériques d’un périphérique connecté qui correspondent à ceux sélectionnés dans la liste de livres,

7. Restaurer des livres récemment supprimés : Permet d’annuler la suppression de livres ou de formats.

Note

Notez que lorsque vous utilisez Supprimer des livres pour supprimer des livres de votre bibliothèque calibre, l’enregistrement du livre est supprimé, mais les livres sont temporairement stockés, pendant quelques jours, dans un dossier poubelle. Vous pouvez annuler la suppression en faisant un clic droit sur le bouton Supprimer des livres et en choisissant de Restaurer des livres récemment supprimés.

Préférences

L’action Préférences vous permet de modifier de diverses façons la manière dont calibre fonctionne. Elle dispose de quatre variations, accessibles en faisant un clic droit sur le bouton.

1. Préférences : vous permet de modifier de diverses façons la manière dont calibre fonctionne. Cliquer sur le bouton effectue aussi cette action.

2. Démarrer l’assistant de bienvenue : Vous permet de démarrer l’Assistant de bienvenue qui apparaît la première fois que vous avez démarré calibre.

3. Obtenir des extensions pour améliorer calibre : Ouvre une nouvelle fenêtre qui affiche les extensions pour calibre. Ces extensions sont développées par des parties tierces pour étendre les fonctionnalités de calibre.

4. Redémarrer en mode dépannage : Vous permet d’activer mode de dépannage qui peut assister les développeurs de calibre à résoudre des problèmes que vous rencontrez avec le programme. Pour la plupart des utilisateurs ceci devraient rester désactivé à moins d’une instruction d’un développeur pour l’activer.

Catalogues

Un catalogue est une collection de livres. calibre peut gérer deux types de catalogues différents :

1. Bibliothèque : C’est une collection de livres stockés dans votre bibliothèque calibre sur votre ordinateur.

2. Périphérique : C’est une collection de livres stockés dans votre lecteur de livre numérique. Elle sera disponible lorsque vous connecterez le lecteur à votre ordinateur.

De nombreuses opérations, telles que l’ajout des livres, la suppression, la visualisation, etc., sont sensibles au contexte. Ainsi, par exemple, si vous cliquez sur le bouton Visualiser quand vous avez le catalogue du Périphérique sélectionné, calibre va ouvrir le fichier sur le périphérique. Si vous avez sélectionné le catalogue de la Bibliothèque sélectionnée, le fichier de votre bibliothèque calibre sera ouvert à la place.

Rechercher et trier

L’option rechercher et trier vous permet d’effectuer un certain nombre d’actions puissantes sur votre collection de livres.

- Vous pouvez les trier par titre, auteur, date, note, etc. en cliquant sur les titres des colonnes.
Vous pouvez également faire des tri sur plusieurs clés, c’est à dire un tri sur plusieurs colonnes. Par exemple, si vous cliquez sur la colonne titre, puis la colonne auteur, le livre sera trié par auteur, puis toutes les entrées pour un même auteur seront triées par titre.

- Vous pouvez rechercher un livre ou un ensemble de livres en utilisant la Barre de recherche. Plus de détails ci-dessous à ce propos.

- Vous pouvez rapidement et facilement modifier les métadonnées en sélectionnant l’entrée que vous souhaitez modifier dans la liste et en appuyant sur la touche E.

- Vous pouvez effectuer des Actions sur des ensembles de livres. Pour sélectionner de multiples livres vous pouvez soit :

- Maintenir la touche Ctrl enfoncée et cliquer sur tous les livres que vous voulez sélectionner

- Maintenir la touche Shift enfoncée et cliquer sur le premier puis sur le dernier livre d’un éventail de livres que vous voulez sélectionner.

- Vous pouvez configurer quels champs (colonnes) vous souhaitez afficher en utilisant le dialogue Préférences.

- Pour effectuer un tri complexe basé sur plusieurs colonnes, ajoutez l’outil Classement par à une barre d’outils via Préférences → Barres d’outils et menus.

L’interface de recherche

Vous pouvez rechercher toutes les métadonnées des livres en saisissant des termes de recherche dans la barre de recherche. Par exemple

Asimov Foundation format:lrf

Cela correspondra à tous les livres dans votre bibliothèque qui ont les mots Asimov et Fondation dans leurs métadonnées et qui sont disponibles dans le format LRF. Quelques exemples de plus:

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

Les types de recherche

Il existe quatre types de recherche : contains, equality, regular expression (voir regular expressions [https://en.wikipedia.org/wiki/Regular_expression]), et character variant. Vous choisissez le type de recherche avec un caractère préfixe.

*Les recherches “contains” *

Les recherches sans caractère de préfixe sont contains et sont par défaut insensibles à la casse. Un élément correspond si la chaîne de recherche apparaît n’importe où dans les métadonnées indiquées. Vous pouvez rendre les recherches contains sensibles à la casse en cochant l’option Recherche sensible à la casse dans Préférences / Recherche. Si l’option de recherche Les caractères non accentués correspondent aux caractères accentués et la ponctuation est ignorée est cochée, alors un caractère correspondra à toutes ses variantes (par exemple, e correspond à é, è, ê, et ë) et toute la ponctuation et les espaces seront ignorés. Par exemple, si l’option Les caractères accentués correspondent à … est cochée, les deux titre s de livres suivants seront pris en compte :

1. Big, Bothéred, and Bad

2. Big Bummer

dès lors ces recherches trouvent :

- title:"er" correspond aux deux (“e” correspond à la fois à “é” et à “e”).

- Title:"g " correspond aux deux car les espaces sont ignorés.

- title:"g," correspond aux deux car la virgule est ignorée.

- title:"gb" correspond aux deux car “, “ est ignoré dans le livre 1 et les espaces sont ignorés dans le livre 2.

- Title:"g b" correspond aux deux car la virgule et l’espace sont ignorés.

- title :"db" correspond à #1 parce que l’espace dans “and Bad” est ignoré.

- Title:"," correspond aux deux (il correspond en fait à tous les livres) car les virgules sont ignorées.

Si l’option Les caractères non accentués correspondent … n’est pas cochée, les variantes de caractères, la ponctuation et l’espacement sont tous significatifs.

Vous ne pouvez définir qu’une seule des options de recherche « Recherche sensible à la casse » et « Les caractères non accentués correspondent aux caractères accentués et la ponctuation est ignorée ».

Les recherches “equality”

Les recherches par égalité sont indiquées en faisant précéder la chaîne de recherche d’un signe égal (=). Par exemple, la requête tag :"=science" correspondra à science, mais pas à science fiction ou hard science`. Les variantes de caractères sont importantes : é ne correspond pas à e.

Deux variantes de recherches d’égalité sont utilisées pour les éléments hiérarchiques (par exemple, A.B.C) : les recherches de préfixes hiérarchiques et les recherches de composants hiérarchiques. La première, indiquée par un seul point après le signe égal (=.) correspond aux parties initiales d’un élément hiérarchique. La seconde, indiquée par deux points après le signe égal (=..), correspond à un nom interne de l’élément hiérarchique. Exemples, en utilisant l’étiquette « Histoire.Militaire.WWII » comme valeur :

- tags:"=.Histoire" : True. Histoire est un préfixe de l’étiquette.

- tags:"=.Histoire.militaire" : True. Histoire.militaire est un préfixe de l’étiquette.

- tags:"=.Histoire.Militaire.WWII" : True. Histoire.Militaire.WWII est un préfixe de l’étiquette, bien qu’impropre.

- tags:"=.Militaire" : True. Militaire est un préfixe de l’étiquette.

- tags :"=.WWII" : False. WWII n’est pas un préfixe d’étiquette.

- tags:"=..Histoire" : True. La hiérarchie contient la valeur Histoire.

- tags:"=..Militaire" : True. La hiérarchie contient la valeur Militaire.

- tags:"=..WWII" : True. La hiérarchie contient la valeur WWII.

- tags:"=..Militaire.WWII" : False. La recherche .. recherche des valeurs uniques.

Recherches “Expressions régulières”

Les recherches par expression régulière sont indiquées en préfixant la chaîne de recherche par un tilde (~). Toute expression régulière compatible avec Python [https://docs.python.org/library/re.html] peut être utilisée. Les barres obliques inverses utilisées pour échapper aux caractères spéciaux dans les expressions régulières doivent être doublées car les barres obliques inverses simples seront supprimées lors de l’analyse des requêtes. Par exemple, pour faire correspondre une parenthèse littérale, vous devez saisir \\( ou bien utiliser les super-guillemets ; (voir ci-dessous). . Les recherches par expression régulière sont des recherches de type « contains », sauf si l’expression est ancrée. Les variantes de caractères sont significatives : ~e ne correspond pas à é.

Recherches de “variantes de caractères”

Les recherches de variantes de caractères sont indiquées en faisant précéder la chaîne de recherche d’un signe d’insertion (^). Cette recherche est similaire à la recherche contains (ci-dessus) sauf que :

- la casse des lettres est toujours ignorée.

- les variantes de caractères correspondent toujours les unes aux autres.

- la ponctuation et les espaces sont toujours significatifs.

Les options de recherche Les caractères non accentués correspondent aux caractères accentués et la ponctuation est ignorée et Recherche sensible à la casse sont ignorées. Elles n’ont aucun effet sur le comportement de cette recherche.

L’exemple suivant compare cette recherche à une recherche avec contenu, en supposant que l’option Les caractères accentués correspondent… est cochée (voir ci-dessus), pour les deux mêmes titres de livres :

1. Big, Bothéred, and Bad

2. Big Bummer

alors ces recherches de variantes de caractères trouvent :

- title:"^er" correspond aux deux (“e” correspond à la fois à “é” et à “e”)

- title:"^g" correspond aux deux

- title:"^g " correspond au numéro 2 car l’espace est significatif

- title:"^g," correspond au numéro 2 car la virgule est significative

- title:"^gb" ne correspond à rien car l’espace et la virgule sont significatifs

- title:"^g b" correspond au numéro 2 car la virgule est significative

- title:"^db" ne correspond à rien

- title:"^," correspond au n°1 (au lieu de tous les livres) car la virgule est significative

Syntaxe des expressions de recherche

Une expression de recherche est une séquence de termes de recherche éventuellement séparés par les opérateurs AND et OR. Si deux termes de recherche sont présents sans opérateur de séparation, le terme AND est supposé. L’opérateur AND a priorité sur l’opérateur OR ; par exemple, l’expression a OR b AND c est identique à a OR (b AND c). Vous pouvez utiliser des parenthèses pour modifier la priorité ; par exemple (a OR b) AND c pour que l’opérateur OR soit évalué avant l’opérateur AND. Vous pouvez utiliser l’opérateur NOT pour annuler (inverser) le résultat de l’évaluation d’une expression de recherche. Exemples :

- not tag:foo trouve tous les livres qui ne contiennent pas la balise foo

- NOT (author:Asimov OR author:Weber) trouve tous les livres qui ne sont pas écrits par Asimov ou Weber.

Les exemples ci-dessus montrent des exemples de termes de recherche. Un terme de recherche de base est une séquence de caractères ne comprenant pas d’espaces, de guillemets ("), de barres obliques inversées (\), ou de parenthèses (( )). Il peut éventuellement être précédé d’un spécificateur de nom de colonne : le nom de recherche d’une colonne suivi de deux points (:), par exemple author:Asimov. Si un terme de recherche doit contenir un espace, alors le terme entier doit être mis entre guillemets, comme dans title : "The Ring". Si le terme recherché doit contenir des guillemets, ils doivent être échappés par des barres obliques inverses. Par exemple, pour rechercher une série nommée La « Boule » et La « Chaîne », utiliser

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

Si vous avez besoin d’une barre oblique inversée, ce qui arrive fréquemment dans les recherches par expressions régulières, utilisez deux barres obliques inversées (\\).

Il est parfois difficile d’obtenir tous les échappements corrects pour que le résultat corresponde à ce que vous voulez, notamment dans les recherches par expression régulière et modèle. Dans ces cas, utilisez la super-quote : """séquence de caractères""". Les caractères surquotés sont utilisés tels quels : aucun traitement d’échappement n’est effectué.

Plus d’information

Pour rechercher une chaîne de caractères qui commence par un signe égal, un tilde ou un caret, préfixez la chaîne de caractères par une barre oblique inverse.

Mettez les chaînes de recherche entre guillemets ( ») si la chaîne contient des parenthèses ou des espaces. Par exemple, pour trouver des livres portant l’étiquette science-fiction, vous devez rechercher tag:"=science fiction". Si vous recherchez tag:=science fiction, vous trouverez tous les livres avec le tag science et le mot fiction dans toutes les métadonnées.

Vous pouvez facilement construire des requêtes de recherche avancées en utilisant le Boîte de dialogue de recherche avancée accessible en cliquant sur le bouton à gauche de la boîte de recherche.

Les champs disponibles pour la recherche sont : tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl et les colonnes personnalisées. Si un périphérique est connecté, le champ ondevice devient disponible, en recherchant la vue bibliothèque de calibre. Pour trouver le nom de recherche (appelée actuellement la consultation de nom) pour une colonne personnalisée, placez votre souris au dessus de l’entête de colonne dans la vue bibliothèque.

Dates

La syntaxe pour rechercher des dates est:

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 date est ambiguë alors la date actuelle en local est utilisée pour la comparaison de date. Par exemple, dans une date locale mm/dd/yyyy 2/1/2009 est interprétée comme 1 Fév 2009. Dans une date locale dd/mm/yyyy elle est interprétée comme 2 Jan 2009. Certaines chaines spéciales de date sont disponibles. La chaîne today traduite en date du jour, quelle qu’elle soit. Les chaînes yesterday et thismonth (ou leur équivalent traduit dans la langue courante) fonctionne aussi. En complément, la chaîne daysago (également traduite) peut être utilisée pour comparer une date à un certain nombre de jours avant. Par exemple

date:>10daysago
date:<=45daysago

Pour éviter des problèmes potentiels avec les chaines traduites lors de l’utilisation d’une version non Anglaise de calibre, les chaînes _today, _yesterday, _thismonth, and _daysago seront toujours disponibles. Elles ne sont pas traduites.

Recherche de dates et de valeurs numériques avec des comparaisons relationnelles

Les champs date et numérique supportent les opérateurs relationnels = (égal), > (plus grand que), >= (plus grand que ou égal à), < (plus petit que), <= (plus petit que ou égal à), et != (différent de). Les champs de notation sont considérés être numérique. Par exemple, la recherche rating:>=3 trouvera les livres notés 3 ou supérieur.

Vous pouvez rechercher des livres qui ont un format d’une certaine taille comme ceci :

- size:>1.1M trouvera les livres dont le format est supérieur à 1.1MB

- size:<=1K trouvera les livres dont le format est inférieur ou égal à 1KB

Vous pouvez rechercher le nombre d’éléments dans des champs à valeurs multiples tels que les étiquettes en utilisant le caractère « # » puis en utilisant la même syntaxe que les champs numériques. Par exemple, pour trouver tous les livres ayant plus de 4 étiquettes, utilisez tags:#>4. Pour trouver tous les livres ayant exactement 10 étiquettes, utilisez tags:#=10.

Indices des séries

Les indices des séries peuvent faire l’objet d’une recherche. Pour les séries standard, le nom de recherche est series_index. Pour les colonnes personnalisées de séries, utilisez le nom de recherche de la colonne suivit par _index. Par exemple, pour rechercher les indices pour une colonne de séries personnalisée #my_series, vous devrez utiliser le nom de recherche #my_series_index. Les indices de séries sont des nombres, donc vous pouvez utiliser les opérateurs relationnels décrits plus haut.

Recherches sauvegardées

Le champ spécial search est utilisé pour Recherches sauvegardées. Si vous enregistrez une recherche avec le nom « Livres de mon conjoint », vous pouvez entrer search:"Livres de mon conjoint" dans la Barre de recherche pour réutiliser la recherche enregistrée. Plus d’informations sur la sauvegarde des recherches ci-dessous.

Bibliothèques virtuelles

Le champ spécial vl est utilisé pour chercher des livres dans une Bibliothèque Virtuelle. Par exemple, vl:Lu trouvera tous les livres dans la Bibliothèque Virtuelle Lu. La recherche vl:Lu and vl:"Science Fiction" trouvera tous les livres qui sont tant dans les Bibliothèques Virtuelles Lu que Science Fiction. La valeur suivant vl: doit être le nom d’une Bibliothèque Virtuelle. Si le nom de la Bibliothèque Virtuelle contient des espaces entourez-le alors avec des guillemets.

Si un champ a une valeur

Vous pouvez rechercher l’absence ou la présence d’une valeur pour un champ en utilisant « true » et « false ». Par exemple :

- cover:false trouve tous les livres sans couverture

- series:true trouve tous les livres qui font partie d’une série

- series:false trouve tous les livres qui ne sont pas dans une série

- comments:false trouve tous les livres avec un commentaire vide

- formats:false trouve tous les livres sans fichier de livre (enregistrements vides).

Colonnes personnalisées oui/non

Recherche de colonnes personnalisées Oui/non pour false, empty, ou blank trouvera tous les livres avec des valeurs indéfinies dans la colonne. Faire une recherche true trouvera les livres qui n’ont pas de valeurs indéfinies dans la colonne. Faire une recherche yes ou checked trouvera tous les livres avec Yes dans la colonne. Faire une recherche no ou unchecked trouvera tous les livres avec No dans la colonne. Notez que les mots yes, no, blank, empty, checked et unchecked sont traduits ; vous pouvez utiliser soit utiliser le mot équivalent dans la langue courante ou le mot anglais. Les mots true et false et les valeurs spéciales _yes, _no, et _empty ne sont pas traduits.

Identifiants

Les identifiants (par exemple, ISBN, DOI, LCCN, etc.) utilisent une syntaxe étendue. Un identificateur a la forme type:valeur, comme dans isbn:123456789. La syntaxe étendue vous permet de spécifier indépendamment le type et la valeur à rechercher. Les deux parties de la requête, type et valeur, peuvent utiliser n’importe quels types de recherche. Exemples :

- identifiers:true trouvera les livres avec n’importe quel identifiant.

- identifiers:false trouvera les livres sans identifiant.

- identifiers:123 recherchera des livres de n’importe quel type qui ont une valeur contenant 123.

- identifiers:=123456789 recherchera des livres de n’importe quel type qui ont une valeur égale à 123456789.

- identifiers:=isbn: et identifiers:isbn:true trouvera les livres avec un type égal à ISBN et ayant n’importe quelle valeur

- indentifiers:=isbn:false trouvera les livres avec un type n’équivalant pas à ISBN.

- identifiers:=isbn:123 trouvera les livres avec un type égal à ISBN ayant une valeur contenant 123.

- identifiers:=isbn:=123456789 trouvera les livres avec un type égal à ISBN et ayant une valeur égale à 123456789.

- identifiers:i:1 trouvera les livres avec un type contenant un i ayant une valeur contenant un 1.

Catégories visibles dans le Navigateur d’étiquettes

La recherche in_tag_browser:true trouve tous les livres qui sont dans les catégories (tags, auteurs, etc.) actuellement affichées dans le Navigateur d’étiquettes. Ceci est utile si vous avez défini les deux préférences Préférences → Apparence et présentation → Navigateur d’étiquettes → Cacher les catégories vides et Trouver et afficher tous les éléments qui correspondent. Une fois ces deux préférences définies, l’exécution de la commande Trouver dans le Navigateur d’étiquettes n’affiche que les catégories contenant des éléments correspondant à la commande Trouver. Ensuite, la recherche in_tag_browser:true trouve en plus les livres avec ces catégories/élements. Vous pouvez facilement lancer cette recherche en appuyant sur la touche Ctrl+Alt+Shift+F ou en cliquant sur le bouton de configuration dans le Navigateur d’étiquettes et en choisissant l’entrée Afficher uniquement les livres dont les catégories sont visibles.

Recherche à l’aide de modèles

Vous pouvez effectuer une recherche en utilisant un modèle dans Le langage de modèle calibre au lieu d’un champ de métadonnées. Pour ce faire, vous saisissez un modèle, un type de recherche et la valeur à rechercher. La syntaxe est la suivante

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

Le template est n’importe quel modèle de langage calibre valide. Le search type doit être l’un des suivants : t (recherche de texte), d (recherche de date), n (recherche numérique), ou b (ensemble/non ensemble (booléen)). La valeur est ce que vous voulez, et peut utiliser les types de recherche décrits ci-dessus pour les différents types de recherche. Vous devez citer la chaîne de recherche entière s’il y a des espaces dans celle-ci.

Exemples :

- template:"program : connected_device_name('main')#@#:t:kindle" – est true lorsque le périphérique kindle est connecté.

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – trouve des livres avec des fichiers EPUB supérieurs à 1 Mo.

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – trouve des livres avec des fichiers EPUB plus récents qu’il y a 10 jours.

- Template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" – trouve tous les livres contenant le nom de la série dans les étiquettes. Cet exemple utilise des guillemets superposés car le modèle utilise à la fois des guillemets simples (') et des guillemets doubles (") lors de la construction de l’expression de recherche.

Vous pouvez construire facilement des modèles de recherche en utilisant le Dialogue de recherche avancée accessible en cliquant sur le bouton . Vous pouvez tester les modèles sur des livres spécifiques en utilisant Testeur de modèle calibre, qui peut être ajouté aux barres d’outils ou aux menus via Préférences → Barres d’outils & menus. Il est également possible de lui attribuer un raccourci clavier via Préférences → Raccourcis.

Boîte de dialogue recherche avancée

Boîte de dialogue recherche avancée

Recherches sauvegardées
calibre vous permet de sauvegarder une recherche fréquemment utilisée sous une dénomination spéciale et ensuite de réutiliser cette recherche avec un simple clic. Pour ce faire, créez votre recherche soit en tapant dans la Barre de recherche ou en utilisant le Navigateur d’étiquettes. Ensuite, tapez le nom que vous souhaitez donner à la recherche dans la zone Recherches Sauvegardées, à droite de la Barre de recherche. Cliquez sur l’icône + à côté de la boîte recherches enregistrées pour sauver la recherche.

Maintenant, vous pouvez accéder à votre recherche sauvegardée dans le Navigateur d’étiquettes sous la rubrique Recherches sauvegardées. Un simple clic vous permettra de réutiliser facilement toute recherche complexe, sans avoir à la recréer.

Recherche dans le texte intégral de tous les livres
calibre peut optionnellement indexer le texte intégral des livres de la bibliothèque pour permettre la recherche instantanée de mots dans n’importe quel livre. Pour utiliser cette fonctionnalité, cliquez sur le bouton FT situé sur le bord gauche de la barre de recherche.

Ensuite, activez l’indexation pour la bibliothèque actuelle. Une fois l’indexation terminée, vous pouvez rechercher tout le texte de la bibliothèque complète. Lorsque vous ajoutez de nouveaux livres à la bibliothèque, ils sont automatiquement indexés en arrière-plan. Cette recherche vous permet de trouver rapidement tous les livres qui contiennent un mot ou une combinaison de mots. Vous pouvez même rechercher des mots qui apparaissent à proximité d’autres mots, comme le montrent les exemples dans la fenêtre contextuelle de recherche. Notez que cet outil de recherche ne trouvera qu’une seule occurrence de la requête dans un livre particulier, et ne listera pas toutes les occurrences. Pour cela, il est préférable de rechercher à l’intérieur du livre en utilisant le calibre Visionneuse de livres numériques.

Vous pouvez réindexer un livre individuel en faisant un clic droit sur le Panneau de détails du livre dans calibre et en choisissant Reindexer ce livre pour une recherche en texte intégral.

Bibliothèques virtuelles

Une Bibliothèque virtuelle est une manière de prétendre que votre bibliothèque calibre a seulement quelques livres plutôt que sa collection complète. C’est une excellente manière de partitionner votre large collection de livres en des entités plus petites, mieux gérable. Pour apprendre comment créer et utiliser des Bibliothèques virtuelles, voir le tutoriel : Bibliothèques virtuelles.

Marquage temporaire des livres

Vous pouvez marquer temporairement des ensembles arbitraires de livres. Les livres marqués auront une épingle sur eux et peuvent être trouvés avec la recherche marked:true. Pour marquer un livre, appuyez sur Ctrl+M ou allez dans Préférences → Barres d’outils & menus et ajoutez le bouton Marquer des livres à la barre d’outils principale.

Vous pouvez marquer les livres avec une étiquette de texte spécifique en faisant un clic droit sur le bouton Marquer les livres et en choisissant Marquer les livres avec une étiquette de texte. Les livres marqués avec des étiquettes de texte peuvent être trouvés plus tard en utilisant la recherche marked :"=le-texte-que-vous-entré".

Deviner les métadonnées à partir des noms de fichier

Normalement, calibre lit les métadonnées à partir du fichier livre, il peut être configuré pour lire les métadonnées à partir du nom de fichier, via Préférences → Importer/exporter → Ajouter des livres → Lire les métadonnées à partir du contenu du fichier.

Vous pouvez aussi contrôler comment les métadonnées sont lues à partir du nom de fichier en utilisant les expressions régulières (voir Tout à propos de l’utilisation des expressions régulières dans calibre). Dans la section Ajouter des livres de la boîte de configuration, vous pouvez spécifier une expression régulière que calibre utilisera pour essayer et deviner les métadonnées à partir des noms de fichier des livres numériques que vous ajoutez à votre bibliothèque. L’expression régulière par défaut est:

title - author

c.-à-d., il suppose que tous les caractères jusqu’au premier `` - `` sont le titre du livre et les caractères suivants sont l’auteur du livre. Par exemple, le nom de fichier

Foundation and Earth - Isaac Asimov.txt

sera interprété avoir le titre Foundation and Earth et l’auteur : Isaac Asimov

Astuce

Si le nom de fichier ne contient pas de trait d’union, l’expression régulière ci-dessus échouera.

Détails du livre

L’écran Détails du livre affiche la couverture et toutes les métadonnées pour le livre actuellement sélectionné. Il peut être caché par l’intermédiaire du bouton Disposition en bas à droite de la fenêtre principale calibre. Le nom de l’auteur affiché dans le panneau Détail du livre peut être cliqué, il vous mènera vers la page Wikipedia au sujet de l’auteur. Ceci peut être personnalisé en faisant un clic droit sur le nom de l’auteur et en sélectionnant Gérer cet auteur.

Similairement, si vous téléchargez des métadonnées pour le livre, panneau Détail du Livre vous affichera des liens pointant vers les pages web du livre sur Amazon, Worldcat, etc. depuis où les métadonnées ont été téléchargées.

Vous pouvez faire un clic droit sur les formats individuels de livre dans le panneau Détail du livre pour les supprimer, les comparer à leurs versions originales, les sauvegarder sur le disque, les ouvrir avec un programme extérieur etc.

Vous pouvez changer la couverture du livre simplement en glissant et déposant une image sur le panneau Détails du Livre. Si vous voulez utiliser un programme tiers pour éditer l’image de couverture, faites un simple clic droit dessus et choisissez Ouvrir avec.

Vous pouvez aussi ajouter des fichiers de livre numérique au livre courant en glissant et déposant les fichiers sur le panneau Détails du livre.

Double cliquer sur le panneau de Détails de livre l’ouvrira dans une fenêtre distincte.

Pour finir, vous pouvez personnaliser exactement quelle information est affichée dans le panneau Détails du livre par l’intermédiaire de Préférences → Apparence → Détails du livre.

Navigateur d’étiquettes

Le Navigateur d’étiquettes vous permet de parcourir facilement votre collection par Auteur/Étiquettes/ Série etc. Si vous cliquez sur n’importe quel élément dans le Navigateur d’étiquettes, par exemple sur le nom d’auteur Isaac Asimov, la liste des livres se limitera à l’affichage des livres de cet auteur. Vous pouvez également cliquer sur les catégories. Par exemple, si vous cliquez sur « Séries » vous affichera tous les livres de n’importe quelle série.

Le premier clic sur un élément permet de limiter la liste des livres à ceux qui contiennent ou correspondent à cet élément. En reprenant l’exemple ci-dessus, en cliquant sur Isaac Asimov vous aurez tous les livres de cet auteur. En cliquant à nouveau sur l’élément, le résultat dépend si l’élément a des sous-éléments ou non (voir les sous-catégories et les éléments hiérarchiques ci-dessous). En continuant l’exemple d’Isaac Asimov en cliquant à nouveau sur Isaac Asimov, vous allez obtenir les livres qui ne sont pas d’Isaac Asimov. Un troisième clic va supprimer la restriction, et vous aurez de nouveau tous les livres. Si vous maintenez enfoncée la touche kbd:Ctrl ou kbd:Maj et cliquez sur plusieurs éléments, la liste prendra en compte toutes les conditions. Par exemple, vous pourriez maintenir la touche Ctrl et cliquez sur les catégories Histoire et Europe pour trouver des livres sur l’histoire européenne.
Le Navigateur d’étiquettes fonctionne en construisant des expressions de recherche qui sont entrées automatiquement dans la barre de recherche. En regardant ce que le Navigateur d’étiquettes génère vous pourrez facilement apprendre à construire des expressions de recherche de base.

Les éléments du Navigateur d’étiquettes ont leurs icônes partiellement colorées. La quantité de couleur dépend de la note moyenne des livres dans cette catégorie. Ainsi, par exemple si les livres d’Isaac Asimov ont une moyenne de quatre étoiles, l’icône Isaac Asimov dans le Navigateur d’étiquettes sera colorée au 4/5e. Vous pouvez passer votre souris sur l’icône pour voir la note moyenne.

Les articles des niveaux externes dans le Navigateur d’étiquettes, comme Auteurs et Séries, sont appelés catégories. Vous pouvez créer vos propres catégories, appelées Catégories utilisateur, qui sont utiles pour organiser les éléments. Par exemple, vous pouvez utiliser l”Éditeur de catégories utilisateur (cliquer le bouton Configurer en bas à gauche du Navigateur d’étiquettes et choisissez Gérer les auteurs, étiquettes etc → Catégories utilisateur`) pour créer une Catégorie utilisateur appelée « Auteurs favoris », mettez alors vos éléments favoris dans la catégorie. Les Catégories utilisateur peuvent avoir des sous catégories. Par exemple, la Catégorie utilisateur « Favoris.Auteurs » est une sous catégorie de Favoris. Vous pouvez aussi créer « Favoris.Séries », dans ce cas il y aura deux sous catégories sous Favoris. Les sous catégories peuvent être créées en faisant un clic droit sur une Catégorie utilisateur, choisir Ajouter une sous catégorie à …, et entrer le nom de la sous catégorie; ou en utilisant l’Éditeur de catégories utilisateur et en entrant les noms comme l’exemple des Favoris ci-dessus.

Vous pouvez rechercher les Catégories utilisateurs de la même manière que les catégories intégrées, en cliquant dessus. Il y a quatre recherches différentes faits un cycle en cliquant sur :

1. « tout ce qui correspond à l’élément dans la catégorie » indiquée par un seul signe plus vert.

2. « tout ce qui correspond à l’élément de la catégorie ou de ses sous-catégories » indiqué par deux signes plus verts.

3. « tout ce qui ne correspond pas à l’élément dans la catégorie » indiquée par un seul signe moins rouge.

4. « tout ce qui ne correspond pas à l’élément dans la catégorie ou dans ses sous-catégories » indiquées par deux signes moins rouges.

Il est également possible de créer des hiérarchies à l’intérieur de certaines catégories texte comme les étiquettes, les séries et les colonnes personnalisées. Ces hiérarchies apparaissent avec un petit triangle, permettant aux sous éléments d’être masqués. Pour utiliser les hiérarchies d’éléments dans une catégorie, vous devez vous rendre tout d’abord dans Préférences → Interface → Apparence → Navigateur d’étiquettes et entrer le(s) nom(s) de catégorie dans le champ « Catégories avec des articles hiérarchiques ». Une fois que c’est fait, les éléments dans cette catégorie qui contiennent des subdivisions seront affichés en utilisant le petit triangle. Par exemple, présumons que vous créez une colonne personnalisée appelée « Genre » et indiquez quelle contient des éléments hiérarchiques. Une fois fait, les éléments tels que Mystère.Thriller et Mystère.Anglais s’afficheront comme Mystère avec le petit triangle à leur suite. Cliquer sur le triangle affichera Thriller et Anglais comme sous éléments. Voir Gérer des sous groupes de livres, par exemple « genre » pour plus d’information.

Les éléments hiérarchiques (éléments avec des sous-éléments) utilisent les quatre recherches “cliquez dessus” identiques à celles des Catégories utilisateurs. Les éléments qui n’ont pas de sous-éléments utilisent deux des recherches : « tout ce qui correspond » et « tout ce qui ne correspond pas ».

Vous pouvez glisser et déposer des éléments dans le Navigateur d’Étiquettes sur les Catégories d’utilisateurs à ajouter à cette catégorie. Si la source est une Catégorie utilisateur, maintenez la touche Maj tout en déplaçant l’article vers la nouvelle catégorie. Vous pouvez aussi glisser et déposer des livres depuis la liste de livres sur les éléments du Navigateur d’étiquettes; déposer un livre sur des éléments dans le Navigateur d’Étiquettes entraînera que cet élément s’appliquera automatiquement au livre déposé. Par exemple, glisser un livre sur Isaac Asimov établira l’auteur pour ce livre à Isaac Asimov. Déposer le sur l’étiquette Histoire ajoutera l’étiquette Histoire aux étiquettes du livre.

Vous pouvez trouver facilement n’importe quel élément dans le Navigateur d’étiquettes en cliquant le bouton de recherche en bas à droite. En complément, vous pouvez faire un clic droit sur n’importe quel élément et choisir une des diverses opérations. Certains exemples sont de le cacher, de le renommer, ou d’ouvrir une boîte de dialogue « Gérer x » qui vous permet de gérer des éléments de ce type. Par exemple, la boîte de dialogue Gérer les auteurs permet de renommer les auteurs et de contrôler comment leurs noms sont triés.

Vous pouvez contrôler comment les éléments sont triés dans le Navigateur d’étiquettes par l’intermédiaire du bouton Configurer en bas à gauche du Navigateur d’étiquettes. Vous pouvez choisir de trier par nom, note moyenne ou popularité (popularité est le nombre de livres avec un élément dans votre bibliothèque; par exemple, la popularité d’Isaac Asimov est le nombre de livres par Isaac Asimov dans votre bibliothèque).

Grille de couvertures
calibre peut vous afficher une grille de couvertures à la place de la liste de livres. Si vous préférez plutôt naviguer dans votre collection par couvertures. La Grille de couvertures est activée en cliquant sur le bouton Disposition grille dans le coin inférieur droit de la fenêtre principale de calibre. Vous pouvez personnaliser la taille des couvertures et l’arrière plan de la Grille de couvertures via Préférences → Interface → Apparence → Grille de couvertures. calibre peut même afficher n’importe quel champ en dessous des couvertures, comme titre, auteurs ou note ou une colonne personnalisée de votre propre invention.

Navigateur de couvertures

En complément de la grille de couvertures décrite plus haut, calibre peut aussi afficher les couvertures en une simple ligne. Ceci est activé par l’intermédiaire d’un bouton Disposition dans le coin inférieur droit de la fenêtre principale. Dans Préférences → Apparence → Navigateur de couvertures vous pouvez changer le nombre de couvertures affichées, et même avoir le navigateur de couverture s’afficher dans une fenêtre distincte.

Ajouter des notes pour les auteurs, les séries, etc.

Vous pouvez ajouter des notes pour un auteur/une série/une étiquette/un éditeur/etc. à votre bibliothèque calibre. Pour ce faire, faites un clic droit sur le nom de l’auteur dans le Navigateur d’étiquettes à gauche ou dans le panneau Détails du livre à droite et choisissez Créer une note ou Éditer la note.

Une simple fenêtre contextuelle vous permet de saisir vos notes en utilisant un formatage de base et en ajoutant des liens et des images. Une fois qu’une note est créée pour un auteur, elle peut être facilement consultée à partir du panneau Détails du livre en cliquant sur le petit crayon à côté du nom de l’auteur.

Vous pouvez rechercher toutes les notes de votre bibliothèque en utilisant l’outil Parcourir les notes en appuyant sur Ctrl+Shift+N ou en l’ajoutant à la barre d’outils via Préférences → Barres d’outils & menus.

Affichage rapide

Parfois vous voulez sélectionner un livre et rapidement obtenir une liste de livres avec la même valeur dans une certaine catégorie (auteurs, étiquettes, éditeur, séries, etc.) comme le livre actuellement sélectionné, mais sans changer la vue actuelle de votre bibliothèque. Vous pouvez faire cela avec Affichage rapide. Affichage rapide ouvre une deuxième fenêtre affichant la liste des livres correspondants à la valeur intéressée. Par exemple, supposons que vous voulez voir une liste de tous les livres avec un ou plus des auteurs du livre actuellement sélectionné. Cliquer dans la cellule de l’auteur qui vous intéresse et appuyez sur la touche “Q” ou cliquez sur l’icône :guilabel:”Affichage rapide” dans la section :guilabel:”Mise en page” de la fenêtre calibre. Une fenêtre ou un panneau s’ouvrira avec tous les auteurs pour ce livre sur la gauche et tous les livres pour l’auteur sélectionné sur la droite.

Quelques exemples des utilisations d’Affichage Rapide : voir rapidement que d’autres livres :

- ont certaine(s) étiquette(s) appliquée(s) au livre actuellement sélectionné,

- sont dans la même série que le livre actuel

- ont les mêmes valeurs dans une colonne personnalisée que le livre actuel

- sont écrits par l’un des mêmes auteurs que le livre actuel

- partager les valeurs dans une colonne personnalisée

Il y deux choix pour où faire apparaître l’information Affichage Rapide :

1. Il peut s’ouvrir « ancré » : au dessus de la fenêtre calibre et il restera ouvert jusqu’à ce que vous le fermiez explicitement.

2. Il peut s’ouvrir « désancré » : comme un panneau dans la section liste de livres de la fenêtre principale de calibre.

Vous pouvez déplacer comme bon vous semble la fenêtre de verrouillé à déverrouillé en utilisant le bouton « Ancrer/Désancrer ».

La panneau Affichage rapide peut être laissé ouvert en permanence, dans ce cas il suivra les mouvements de la liste de livres. Par exemple, si dans la vue bibliothèque calibre vous cliquez sur une colonne de catégorie (étiquettes, séries, éditeur, auteur, etc.) pour un livre, le contenu de la fenêtre Affichage rapide changera pour vous montrer dans le panneau de gauche les éléments dans cette catégorie pour le livre sélectionné (par exemple, les étiquettes pour ce livre). Le premier élément dans cette liste sera sélectionné, et Affichage rapide vous montrera dans le panneau de droite tous les livres dans votre bibliothèque qui utilisent cet valeur. Cliquer sur une valeur différente dans le panneau de gauche pour voir les livres avec cette valeur différente.

Double cliquez sur un livre dans la fenêtre Affichage rapide pour sélectionner ce livre dans la vue bibliothèque. Ceci changera aussi les éléments dans la fenêtre Affichage rapide (panneau de gauche) pour montrer les éléments dans le livre nouvellement sélectionné.

Shift- ou Ctrl- double clic sur un livre dans la fenêtre Affichage rapide pour ouvrir la boîte de dialogue modifier les métadonnées de ce livre dans la fenêtre calibre. Le livre modifié sera Affiché rapidement quand vous fermerez la boite de dialogue Modifier les métadonnées.

Vous pouvez voir si une colonne peut être Affichée Rapidement en passant votre souris sur l’entête de la colonne et en regardant l’info-bulle pour cet entête. Vous pouvez aussi le savoir en faisant un clic droit sur l’entête de colonne pour voir si l’option « Affichage rapide » apparaît dans le menu, dans un tel cas choisir cette option Affichage rapide est équivalent à appuyer “Q” dans la cellule actuelle.

Options (dans :guilabel:`Préférences->Apparence et présentation->Affichage rapide) :

- Respecte (ou non) la Bibliothèque virtuelle actuelle. Si coché, alors Affichage rapide montre uniquement les livres dans la Bibliothèque virtuelle actuelle. Par défaut : respecte les Bibliothèques virtuelles

- Changer le contenu de la fenêtre Affichage rapide quand la colonne est changée dans la liste de livres en utilisant les touches curseur. Par défaut : ne suit pas les changements effectués avec les touches curseur

- Changer la colonne qui est « Affiché rapidement » quand une cellule dans la fenêtre Affichage rapide est double cliquée. Autrement le livre est changé mais la colonne examinée ne l’est pas. Par défaut : changer la colonne

- Changer la colonne qui est « Affichée rapidement » vers la colonne actuelle quand la touche retour est appuyée dans le panneau Affichage rapide. Autrement le livre est changé mais la colonne qui est examinée ne l’est pas. Par défaut : changer la colonne

- Choisir quelles colonnes sont affichées dans la/le fenêtre/panneau Affichage rapide

Tâches

Le panneau des Travaux montre le nombre des tâches en cours. Les travaux sont des tâches qui s’exécutent dans un processus séparé. Ils incluent la conversion de livres numériques et les échanges avec votre liseuse. Vous pouvez cliquer sur le panneau des tâches pour accéder à la liste des tâches. Une fois qu’une tâche est terminée vous pouvez voir un journal détaillé de cette tâche en la double cliquant dans cette liste. Ceci est utile pour dépanner des tâches qui ne se sont pas terminées avec succès.

Raccourcis clavier
calibre a plusieurs raccourcis clavier pour vous faire gagner du temps et du déplacement de souris. Ces raccourcis sont disponibles depuis la liste des livres (quand vous n’êtes pas occupé à éditer les détails d’un livre particulier), et la plupart d’entre eux affectent le titre que vous avez sélectionné. La visionneuse de livre numérique calibre a ses propres raccourcis qui peuvent être personnalisés en cliquant sur le bouton Préférences dans la visionneuse.

Note

Remarque : Les raccourcis clavier calibre ne requièrent pas de touche modificatrice (Command, Option, Control, etc.) sauf si cela est spécialement indiqué. Vous avez seulement besoin d’appuyer la touche de lettre. Par ex. : E pour éditer.

Raccourcis clavier pour le programme principal de calibre

Raccourci clavier

Action

F2 (Enter pour macOS)

Editer les métadonnées du champ actuellement sélectionné dans la liste de livres.

A

Ajouter des livres

Shift+A

Ajouter des formats aux livres sélectionnés

C

Convertir les livres sélectionnés

D

Envoyer vers le périphérique

Del

Supprimer les livres sélectionnés

E

Editer les métadonnées des livres sélectionnés

G

Obtenir des livres

I

Afficher Détails du livre

K

Modifier la table des matières

M

Fusionner les enregistrements sélectionnés

Alt+M

Fusionner les enregistrements sélectionnés, garder les originaux

O

Ouvrir le répertoire contenant

P

Polir des livres

S

Enregistrer sous…

T

Editer le livre

V

Visualiser

Shift+V

Voir le dernier livre lu

Alt+V/Cmd+V pour macOS

Visualiser un format spécifique

Alt+Shift+J

Afficher/Masquer la liste des tâches

Alt+Shift+B

Afficher/Masquer le Navigateur de Couvertures

Alt+Shift+D

Afficher/Masquer le panneau des Détails du Livre

Alt+Shift+T

Afficher/Masquer Le Navigateur d’Étiquette

Alt+Shift+G

Afficher/Masquer la grille de couvertures

Alt+A

Afficher les livres du même auteur que le livre actuel

Alt+T

Afficher des livres avec les mêmes étiquettes que le livre actuel

Alt+P

Afficher des livres avec le même éditeur que le livre actuel

Alt+Shift+S

Afficher des livres dans la même série que le livre actuel

/, Ctrl+F

Focalisez la barre de recherche

Shift+Ctrl+F

Ouvrir la boîte de dialogue Recherche avancée

Shift+Alt+F

Basculer la barre de recherche

Esc

Vider la recherche courante

Shift+Esc

Ciblez la liste des livres

Ctrl+Esc

Nettoyez la Bibliothèque virtuelle

Alt+Esc

Nettoyez la restriction additionnelle

Ctrl+*

Créer une Bibliothèque virtuelle temporaire basée sur la recherche actuelle

Ctrl+Droit

Sélectionne le prochain volet de la Bibliothèque virtuelle

Ctrl+Gauche

Sélectionne le volet précédent de la Bibliothèque virtuelle

N ou F3

Trouver le livre suivant qui correspond à la recherche actuelle (fonctionne uniquement si la la mise en évidence de la recherche est activée dans les préférences de recherche)

Shift+N ou Shift+F3

Trouver le livre précédent qui correspond à la recherche actuelle (fonctionne uniquement si la la mise en évidence de la recherche est activée dans les préférences de recherche)

Ctrl+D

Télécharger les métadonnées et les couvertures

Ctrl+R

Redémarrer calibre

Ctrl+Shift+R

Redémarrer calibre en mode dépannage

Shift+Ctrl+E

Ajouter des livres à calibre

Ctrl+M

Basculer entre le statut marqués/démarqués sur les livres sélectionnés

Ctrl+/ ou Ctrl+Alt+F

Ouvrez la fenêtre popup pour rechercher le texte intégral de tous les livres de la bibliothèque

Q

Ouvre la fenêtre d’Affichage Rapide pour voir les livres en rapport aux séries/étiquettes/etc.

Shift+Q

Se concentre sur le panneau d’Affichage Rapide ouvert

Shift+S

Effectue une recherche dans le panneau d’Affichage Rapide

F5

Réappliquer le tri actuel

Ctrl+Q

Quitter calibre

X

Activer le défilement automatique de la liste des livres

Ctrl+Alt+Shift+F

Restreindre les livres affichés aux seuls livres appartenant à une catégorie actuellement affichée dans le Navigateur d’étiquettes.

B

Parcourir les annotations (mise en évidence et signets) faites dans la visionneuse calibre pour tous les livres de la bibliothèque

Ctrl+Shift+N

Parcourir les notes associées aux auteurs/séries/étiquettes/etc.

Alt+Shift+L

Basculer la disposition entre les vues larges et étroites

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Ajouter votre site web favori d’actualités

Ajouter votre site web favori d’actualités
calibre contient une structure de création puissante, flexible et facile à utiliser pour télécharger des actualités sur Internet et les convertir en livre numérique. Ce qui suit vous montrera, au moyen d’exemples, comment obtenir les actualités de divers sites web.

Pour avoir une compréhension de la façon d’employer cette structure de création, suivez les exemples ci-dessous dans l’ordre :

- Récupération entièrement automatique

- Le blog calibre

- bbc.co.uk

- Personnaliser le processus de récupération

- Utilisation de la version imprimable de bbc.co.uk

- Remplacer les styles d’article

- Découper et émincer

- Exemple de la vie réelle

- Astuces pour développer de nouvelles recettes

- Lectures recommandées

- Documentation de l’API

Récupération entièrement automatique

Si votre source d’actualité est assez simple, calibre est capable d’en effectuer la récupération de manière complètement automatique. Tout ce que vous avez à faire est de fournir l’URL. calibre recueille toutes les informations nécessaires pour télécharger une source d’actualités dans une recette. Pour intégrer à calibre une nouvelle source d’actualité, vous devez créer une recette pour celle-ci. Voyons quelques exemples :

Le blog calibre

Le blog calibre est un blog de postes qui décrivent beaucoup de fonctionnalités utiles de calibre d’une manière simple et accessible pour les nouveaux utilisateurs de calibre. Pour pouvoir télécharger ce blog dans un livre numérique, nous nous référons au flux RSS du blog:

http://blog.calibre-ebook.com/feeds/posts/default

J’ai obtenu l’URL RSS en regardant sous « Subscribe to » en bas de la page du blog et en choisissant Posts → Atom. Pour faire en sorte que calibre télécharge les flux et les convertissent en livre numérique, vous devrez faire une clic droit sur le bouton Récupérer des actualités et puis le bouton Ajouter une source personnalisée d’informations. Une boite de dialogue semblable à celle montrée ci-dessous devrait s’ouvrir.

Tout d’abord entrez Blog calibre dans le champ Titre de la recette. Ceci sera le titre du livre numérique qui sera créé à partir des articles des flux ci-dessus.

Les deux champs suivants (Article le plus ancien et Max. Nombre d’articles par flux) donnent la possibilité de contrôler combien d’articles pourront être téléchargés de chaque flux. Ils sont assez explicites.

Pour ajouter les flux à la recette, entrer le titre du flux et l’URL du flux et cliquez sur le bouton Ajouter un flux. Une fois que vous avez ajouté le flux, cliquez simplement sur le bouton Sauvegarder et vous avez fini ! Fermez la boîte de dialogue.

Pour tester votre nouvelle recette, cliquez le bouton Récupérer des informations et dans le sous menu Personnaliser les sources d’actualités cliquez guilabel:Blog calibre. Après quelques minutes, le nouveau livre numérique de postes de blog apparaîtra dans la vue de la bibliothèque principale (si votre lecteur est connecté, il sera déposé sur le lecteur plutôt que dans la bibliothèque). Sélectionnez-le et appuyer le bouton Visualiser pour le lire !

La raison pour laquelle cela fonctionne si bien, avec si peu d’efforts, est parce que le blog fournit un flux RSS à contenu plein, c’est à dire, dont le contenu de l’article est inclut dans le flux lui-même. Pour la plupart des sites d’actualités qui fournissent des actualités de cette façon, avec des flux à contenu plein, vous n’avez pas besoin de faire beaucoup plus d’efforts pour les convertir en livres numériques. Maintenant nous allons regarder à des sources d’actualités qui ne fournissent pas des flux à contenu plein. Dans de tels flux, L’article complet est une page web et le flux contient uniquement un lien vers la page web avec un petit résumé de l’article.

bbc.co.uk

Essayons les deux flux suivants de The BBC :

1. News Front Page: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml

2. Science/Nature: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml

Suivez la procédure comme exposée dans Le blog calibre pour créer une recette pour The BBC (en utilisant les flux ci-dessus). Regardez le livre numérique téléchargé,nous voyons que calibre a réalisé un travail honorable d’extraire seulement le contenu qui vous intéresse depuis la page web de chaque article. Cependant, le processus d’extraction n’est pas parfait. Parfois il laisse du contenu indésirable comme des menus et des aides à la navigation ou enlève du contenu qui aurait dû apparaître seul, comme des titres d’article. Pour obtenir une extraction parfaite, nous aurons besoin d’adapter le processus de récupération, tel que décrit dans la section suivante.

Personnaliser le processus de récupération

Quand vous voulez perfectionner le processus de téléchargement, ou télécharger du contenu d’un site web particulièrement complexe, vous pouvez vous servir de toutes la puissance et de la flexibilité de la structure recette. Afin de faire cela, dans la boîte de dialogue Ajouter des sources d’actualités personnalisées, cliquez simplement sur le bouton Basculer vers le mode Avancé.

La personnalisation la plus facile et souvent la plus productive est d’employer la version imprimable des articles en ligne. La version imprimable est habituellement moins compliqué et se traduit beaucoup plus souplement en un livre numérique. Essayons d’employer la version imprimable des articles de The BBC.

Utilisation de la version imprimable de bbc.co.uk

La première étape est de regarder le livre numérique que nous avons précédemment téléchargé depuis bbc.co.uk. A la fin de chaque article, dans le livre numérique il y a un petit texte de présentation vous indiquant d’où l’article a été téléchargé. Copier et coller cette URL dans un navigateur. Maintenant sur la page Web de l’article recherchez un lien qui pointe vers la « version imprimable ». Cliquez le pour voir la version imprimable de l’article. Il parait beaucoup plus ordonné ! Comparez maintenant les deux URLs. Pour moi elles étaient :

URL d’article

https://news.bbc.co.uk/2/hi/science/nature/7312016.stm

URL de version imprimable

https://newsvote.bbc.co.uk/mpapps/pagetools/print/news.bbc.co.uk/2/hi/science/nature/7312016.stm

Aussi il semble que pour obtenir la version imprimable, nous avons besoin de préfixer que URL d’article avec :

newsvote.bbc.co.uk/mpapps/pagetools/print/

Dorénavant dans le Mode Avancé de la boîte de dialogue Personnaliser les sources d’actualités, vous devriez voir quelque chose comme (rappeler vous de sélectionner la recette The BBC avant de basculer vers le mode avancé) :

Vous pouvez voir que les champs du Mode de base ont été traduits en code de Python d’une façon simple. Nous devons ajouter des instructions à cette recette pour employer la version imprimable des articles. Tout ce qui est nécessaire est d’ajouter les deux lignes suivantes :

def print_version(self, url):
return url.replace('https://', 'https://newsvote.bbc.co.uk/mpapps/pagetools/print/')

C’est du Python, donc l’indentation est importante. Après que vous ayez ajouté les lignes, elles devraient ressembler à :

Ci-dessus, def print_version(self, url) définit une méthode qui est appelée par calibre pour chaque article. url est l’URL de l’article original. Ce que print_version fait est de prendre cette url et de la remplacer avec l’URL qui pointe vers la version imprimable de l’article. Pour apprendre à propos de Python [https://www.python.org] voir le tutoriel [https://docs.python.org/tutorial/].

Maintenant, cliquez sur le bouton Ajouter/Mettre à jour la recette et vos changements seront enregistrés. Téléchargez à nouveau le livre numérique. Vous devriez obtenir un livre numérique beaucoup plus amélioré. Un des problèmes avec la nouvelle version est que les polices sur la page Web de la version imprimable sont trop petites. Ceci est automatiquement fixé en convertissant en livre numérique, mais même après le processus de fixation, la taille de la police des menus et la barre de navigation peuvent être trop grandes relativement au texte de l’article. Pour fixer ceci, nous ferons encore plus de personnalisation dans la prochaine section.

Remplacer les styles d’article

Dans la section précédente, nous avons vu que la taille de la police pour des articles de la version imprimable de The BBC était trop petite. Dans la plupart des sites Web, le The BBC inclus, cette taille de la police est placée au moyen de feuilles de style CSS. Nous pouvons désactiver la récupération de telles feuilles de style n ajoutant la ligne:

no_stylesheets = True

La recette ressemble maintenant à :

La nouvelle version parait assez bonne. Si vous êtes un perfectionniste, vous voudrez lire la prochaine section, qui traite de modifier réellement le contenu téléchargé.

Découper et émincer
calibre a des capacités très puissantes et flexibles quand il s’agit de manipuler le contenu téléchargé. Pour montrer quelques uns de ces derniers, regardons encore notre vieille amie la recette The BBC. Regardez le code source (HTML) de quelques articles (version imprimable), nous voyons qu’il contient un pied de page qui n’apporte aucune information utile

<div class="footer">
...
</div>

Ceci peut être supprimé en ajoutant:

remove_tags = [dict(name='div', attrs={'class':'footer'})]

à la recette. Finalement, remplaçons certains des CSS que nous avons désactivés plus tôt, avec notre propre CSS qui est plus adapté pour la conversion en livre numérique:

extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'

Avec ces ajouts, notre recette est devenue « qualité de production ».

Cette recette explore seulement la partie émergée de l’iceberg quand il s’agit de la puissance de calibre. Pour mieux explorer les capacités de calibre nous examinerons un exemple plus complexe de vie réelle dans la prochaine section.

Exemple de la vie réelle

Un exemple de la vie réelle raisonnablement complexe qui expose plus de l”API de BasicNewsRecipe est la recette pour The New York Times
import string, re from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup class NYTimes(BasicNewsRecipe):

title = 'The New York Times'
__author__ = 'Kovid Goyal'
description = 'Daily news from the New York Times'
timefmt = ' [%a, %d %b, %Y]'
needs_subscription = True remove_tags_before = dict(id='article')
remove_tags_after = dict(id='article')
remove_tags = [dict(attrs={'class':['articleTools', 'post-tools', 'side_tool', 'nextArticleLink clearfix']}), dict(id=['footer', 'toolsRight', 'articleInline', 'navigation', 'archive', 'side_search', 'blog_sidebar', 'side_tool', 'side_index']), dict(name=['script', 'noscript', 'style'])]
encoding = 'cp1252'
no_stylesheets = True extra_css = 'h1 {font: sans-serif large;}\n.byline {font:monospace;}'

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 def parse_index(self):
soup = self.index_to_soup('https://www.nytimes.com/pages/todayspaper/index.html')

def feed_title(div):
return ''.join(div.findAll(text=True, recursive=False)).strip()

articles = {}
key = None ans = []
for div in soup.findAll(True, attrs={'class':['section-headline', 'story', 'story headline']}):

if ''.join(div['class']) == 'section-headline':
key = string.capwords(feed_title(div))
articles[key] = []
ans.append(key)

elif ''.join(div['class']) in ['story', 'story headline']:
a = div.find('a', href=True)
if not a:
continue url = re.sub(r'\?.*', '', a['href'])
url += '?pagewanted=all'
title = self.tag_to_string(a, use_alt=True).strip()
description = ''
pubdate = strftime('%a, %d %b')
summary = div.find(True, attrs={'class':'summary'})
if summary:
description = self.tag_to_string(summary, use_alt=False)

feed = key if key is not None else 'Uncategorized'
if feed not in articles:
articles[feed] = []
if not 'podcasts' in url:
articles[feed].append( dict(title=title, url=url, date=pubdate, description=description, content=''))
ans = self.sort_index_by(ans, {'The Front Page':-1, 'Dining In, Dining Out':1, 'Obituaries':2})
ans = [(key, articles[key]) for key in ans if key in articles]
return ans def preprocess_html(self, soup):
refresh = soup.find('meta', {'http-equiv':'refresh'})
if refresh is None:
return soup content = refresh.get('content').partition('=')[2]
raw = self.browser.open('https://www.nytimes.com'+content).read()
return BeautifulSoup(raw.decode('cp1252', 'replace'))

Nous voyons plusieurs nouvelles fonctionnalités dans cette recipe. En premier, nous avons:

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

Ceci règle le temps affiché sur la page de garde du livre numérique créé au format , Day, Day_Number Month, Year. Voir timefmt.

Voyons maintenant un groupe de directives pour nettoyer l”:term:` HTML` téléchargé:

remove_tags_before = dict(name='h1')
remove_tags_after = dict(id='footer')
remove_tags = ...

Celle-ci supprime tout avant la première balise <h1> et tout ce qui se trouve après le première balise dont l’id est footer. Voir remove_tags, remove_tags_before, remove_tags_after.

La prochaine fonctionnalité intéressante est:

needs_subscription = True
...
def get_browser(self):
...

needs_subscription = True dit à calibre que cette recette nécessite un nom d’utilisateur et un mot de passe pour accéder au contenu. Ceci amène calibre à demander après un nom d’utilisateur et un mot de passe à chaque fois que vous essayez d’utiliser cette recette. Le code dans calibre.web.feeds.news.BasicNewsRecipe.get_browser() fait réellement l’ouverture de session sur le site Web de NYT. Une fois connecté, calibre utilisera la même instance, connectée, du navigateur pour récupérer tout le contenu. Voir mechanize [https://mechanize.readthedocs.io/en/latest/] pour comprendre le code dans get_browser.

Une autre nouvelle fonctionnalité est la méthode calibre.web.feeds.news.BasicNewsRecipe.parse_index(). Sont travail est d’aller sur https://www.nytimes.com/pages/todayspaper/index.html et de récupérer la liste des articles qui apparaissent dans le journal du jour. Beaucoup plus complexe que d’utiliser simplement le RSS, la recette crée un livre numérique qui correspond pratiquement au journal du jour. parse_index fait une forte utilisation de BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] pour examiner la page web quotidienne. Vous pouvez aussi en utiliser d’autres, de plus modernes analyseurs si vous n’aimez pas BeautifulSoup. calibre est fourni avec lxml [https://lxml.de/] et html5lib [https://github.com/html5lib/html5lib-python], qui sont les analyseurs recommandés. Pour les utiliser, remplacer l’appel à index_to_soup() avec le suivant:

raw = self.index_to_soup(url, raw=True)
# For html5lib
import html5lib
root = html5lib.parse(raw, namespaceHTMLElements=False, treebuilder='lxml')
# For the lxml html 4 parser
from lxml import html
root = html.fromstring(raw)

La nouvelle fonctionnalité finale est la méthode calibre.web.feeds.news.BasicNewsRecipe.preprocess_html(). Elle peut être employée pour exécuter des transformations quelconques sur chaque page HTML téléchargée. Ici elle est employée pour éviter les publicités que le nytimes place avant chaque article.

Astuces pour développer de nouvelles recettes

La meilleure manière de développer de nouvelles recettes est d’utiliser l’interface de commande en ligne. Créer la recette en utilisant votre éditeur Python favori et sauvegardez la dans un fichier nommé myrecipe.recipe. L’extension .recipe est obligatoire. Vous pouvez télécharger le contenu qu’utilise cette recette avec la commande:

ebook-convert myrecipe.recipe .epub --test -vv --debug-pipeline debug

La commande ebook-convert téléchargera toutes les pages web et les enregistrera dans le fichier EPUB myrecipe.epub. L’option -vv fait en sorte que ebook-convert renvoie beaucoup d’informations sur ce qu’il fait. L’option ebook-convert-recipe-input --test fait qu’il ne télécharge que quelques articles d’au maximum deux flux. En outre, ebook-convert déposera l’HTML téléchargé dans le répertoire debug/input, où debug est le répertoire que vous avez spécifié dans l’option ebook-convert --debug-pipeline.

Une fois le téléchargement terminé, vous pouvez regarder l’HTML téléchargé en ouvrant le fichier debug/input/index.html dans un navigateur. Une fois que vous êtes satisfait que le téléchargement et le pré-traitement se sont déroulés correctement, vous pouvez générer des livres numériques dans différents formats comme montré ci-dessous:

ebook-convert myrecipe.recipe myrecipe.epub
ebook-convert myrecipe.recipe myrecipe.mobi
...

Si vous êtes satisfait de votre recette et que vous sentez qu’il y a suffisamment de demandes pour l’inclure dans le jeu de recettes intégrées, déposez votre recette sur le forum calibre de recettes [https://www.mobileread.com/forums/forumdisplay.php?f=228] pour la partager avec les autres utilisateurs de calibre.

Note

Sous macOS, les outils en de ligne de commande sont à l’intérieur de la suite logicielle calibre, par exemple, si vous installez calibre dans /Applications les outils de ligne de commande sont dans /Applications/calibre.app/Contents/MacOS/.

Voir aussi

ebook-convert

L’interface en ligne de commande pour toute conversion de livre numérique

Lectures recommandées

Pour en apprendre plus sur l’écriture avancée de recettes utilisant quelques unes des facilités disponibles dans BasicNewsRecipe, vous devriez consulter les sources suivantes :

Documentation API

Documentation sur la classe BasicNewsRecipe et toutes ses méthodes importantes et champs.

BasicNewsRecipe [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/web/feeds/news.py]

Le code source de BasicNewsRecipe

Recettes intégrées [https://github.com/kovidgoyal/calibre/tree/master/recipes]

Le code source des recettes intégrées qui est fourni avec calibre

Le forum de recettes calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228]

Un bon nombre d’auteurs bien informés des recettes calibre traînent ici.

Documentation de l’API

Documentation API pour les recettesBasicNewsRecipe

BasicNewsRecipe.abort_article()
BasicNewsRecipe.abort_recipe_processing()
BasicNewsRecipe.add_toc_thumbnail()
BasicNewsRecipe.adeify_images()
BasicNewsRecipe.canonicalize_internal_url()
BasicNewsRecipe.cleanup()
BasicNewsRecipe.clone_browser()
BasicNewsRecipe.default_cover()
BasicNewsRecipe.download()
BasicNewsRecipe.extract_readable_article()
BasicNewsRecipe.get_article_url()
BasicNewsRecipe.get_browser()
BasicNewsRecipe.get_cover_url()
BasicNewsRecipe.get_extra_css()
BasicNewsRecipe.get_feeds()
BasicNewsRecipe.get_masthead_title()
BasicNewsRecipe.get_masthead_url()
BasicNewsRecipe.get_obfuscated_article()
BasicNewsRecipe.get_url_specific_delay()
BasicNewsRecipe.image_url_processor()
BasicNewsRecipe.index_to_soup()
BasicNewsRecipe.is_link_wanted()
BasicNewsRecipe.parse_feeds()
BasicNewsRecipe.parse_index()
BasicNewsRecipe.populate_article_metadata()
BasicNewsRecipe.postprocess_book()
BasicNewsRecipe.postprocess_html()
BasicNewsRecipe.preprocess_html()
BasicNewsRecipe.preprocess_image()
BasicNewsRecipe.preprocess_raw_html()
BasicNewsRecipe.print_version()
BasicNewsRecipe.publication_date()
BasicNewsRecipe.skip_ad_pages()
BasicNewsRecipe.sort_index_by()
BasicNewsRecipe.tag_to_string()
BasicNewsRecipe.articles_are_obfuscated
BasicNewsRecipe.auto_cleanup
BasicNewsRecipe.auto_cleanup_keep
BasicNewsRecipe.center_navbar
BasicNewsRecipe.compress_news_images

BasicNewsRecipe.compress_news_images_auto_size

BasicNewsRecipe.compress_news_images_max_size
BasicNewsRecipe.conversion_options
BasicNewsRecipe.cover_margins
BasicNewsRecipe.delay
BasicNewsRecipe.description
BasicNewsRecipe.encoding
BasicNewsRecipe.extra_css
BasicNewsRecipe.feeds
BasicNewsRecipe.filter_regexps
BasicNewsRecipe.handle_gzip
BasicNewsRecipe.ignore_duplicate_articles
BasicNewsRecipe.keep_only_tags
BasicNewsRecipe.language
BasicNewsRecipe.masthead_url
BasicNewsRecipe.match_regexps
BasicNewsRecipe.max_articles_per_feed
BasicNewsRecipe.needs_subscription
BasicNewsRecipe.no_stylesheets
BasicNewsRecipe.oldest_article
BasicNewsRecipe.preprocess_regexps
BasicNewsRecipe.publication_type
BasicNewsRecipe.recipe_disabled
BasicNewsRecipe.recursions
BasicNewsRecipe.remove_attributes
BasicNewsRecipe.remove_empty_feeds
BasicNewsRecipe.remove_javascript
BasicNewsRecipe.remove_tags
BasicNewsRecipe.remove_tags_after
BasicNewsRecipe.remove_tags_before
BasicNewsRecipe.requires_version
BasicNewsRecipe.resolve_internal_links
BasicNewsRecipe.reverse_article_order
BasicNewsRecipe.scale_news_images
BasicNewsRecipe.scale_news_images_to_device
BasicNewsRecipe.simultaneous_downloads
BasicNewsRecipe.summary_length
BasicNewsRecipe.template_css
BasicNewsRecipe.timefmt
BasicNewsRecipe.timeout
BasicNewsRecipe.title
BasicNewsRecipe.use_embedded_content

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »

- Ajouter votre site web favori d’actualités »

- Documentation API pour les recettes

Documentation API pour les recettes

L’API pour l’écriture des recettes est définie par BasicNewsRecipe
class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[source]

Classe de base qui contient toute la logique nécessaire dans toutes les recettes. En comprenant progressivement plus des fonctionnalités de cette classe, vous pouvez progressivement faire des recettes plus personnalisées/plus puissantes. Pour un tutoriel d’introduction sur la création des recettes, voir Ajouter votre site web favori d’actualités.

abort_article(msg=None)[source]

Appelle cette méthode à l’intérieur de l’une des méthodes pré-traitée pour avorter le téléchargement de l’article actuel. Utile pour sauter les articles qui contiennent du contenu inadéquat, tel que les articles vidéos purs.

abort_recipe_processing(msg)[source]

Entraîne le système de téléchargement de recette à abandonner le téléchargement de cette recette, affichant un simple message d’explication à l’utilisateur.

add_toc_thumbnail(article, src)[source]

Appelez ceci à partir de populate_article_metadata avec l’attribut src d’une étiquette <img> de l’article qu’il est approprié d’utiliser comme la vignette représentant l’article dans la Table des Matières. Si la vignette qui est actuellement utilisée est dépendante du périphérique (actuellement uniquement utilisé par les Kindle). Notez que l’image référencée doit être une image qui a été téléchargée avec succès, autrement elle sera ignorée.

classmethod adeify_images(soup)[source]

Si votre recette quand elle est convertie en EPUB a des problèmes avec les images quand il est vu dans Adobe Digital Editions, appelez cette méthode depuis postprocess_html().

canonicalize_internal_url(url, is_link=True)[source]

Renvoie un ensemble de représentations classiques de url. L’implémentation par défaut utilise juste le hostname du serveur et le chemin de l’URL, ignorant tout paramètres de requête, fragments, etc. Les représentations classiques doivent être uniques tout au long des URLs pour cette source d’actualités. Si elles ne le sont pas, alors les liens internes peuvent être incorrectement résolus.

Paramètres:

is_link – Est True si l’URL provient d’un lien interne dans un fichier HTML. False si l’URL est l’URL utilisée pour télécharger un article.

cleanup()[source]

Appelé après que tous les articles aient été téléchargés. Utilisez-le pour faire n’importe quel nettoyage comme se déconnecter des sites d’abonnement, etc.

clone_browser(br)[source]

Clonez le navigateur. Les navigateurs clonés sont utilisés pour les téléchargements en parallèle, puisque mechanize n’est pas un thread sécurisé. Les routines par défaut de clonage devraient capturer la plupart des personnalisations de navigateur, mais si vous faites quelque chose d’exotique dans votre recette, vous devriez outrepasser cette méthode dans votre recette et cloner manuellement.

Les instances navigateurs clonées utilisent le même, thread sécurisé CookieJar par défaut, à moins que vous n’ayez adapté la manipulation des cookies.

default_cover(cover_file)[source]

Créer une couverture générique pour les recettes qui n’ont pas de couverture

download()[source]

Téléchargez et prétraitez tous les articles des flux dans cette recette. Cette méthode devrait être appelée uniquement une fois sur une instance particulière de recette. L’appeler plus d’une fois mènera à un comportement non défini. :return: Path to index.html

extract_readable_article(html, url)[source]

Extrait le contenu de l’article principal à partir de “html”, nettoie et retourne comme un tuple (article_html, extracted_title). Basé sur l’algorithme original de lisibilité par Arc90.

get_article_url(article)[source]

Contourne dans une sous-classe pour personnaliser l’extraction de l”URL qui pointe vers le contenu de chaque article. Retourne l’URL de l’article. Il est appelé avec article, un objet représentant un article analysé depuis un plux. Voir feedparser [https://pythonhosted.org/feedparser/]. Par défaut il regarde après le lien original (pour les flux syndiqués par l’intermédiaire d’un service comme PeedBurner ou Pheedo) et si trouvé, retourne cela ou renvoie à article.link [https://pythonhosted.org/feedparser/reference-entry-link.html].

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

Retourne une instance de navigateur utilisée pour récupérer des documents à partir du web. Par défaut, retourne une instance de navigateur mechanize [https://github.com/jjlee/mechanize] qui supporte les cookies, ignore robots.txt, traite les rafraîchissements et a un agent utilisateur mozilla firefox.

Si votre recette requiert que vous vous connectiez au préalable, outrepasser cette méthode dans votre sous-classe. Par exemple, le code suivant est utilisé dans la recette New York Times pour se connecter avec un accès complet:

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()[source]

Renvoie une URL à l’image de couverture pour cette question ou None. Par défaut il retourne la valeur du membre self.cover_url qui est normalement None. Si vous voulez que votre recette télécharge une couverture pour le livre numérique outrepassez cette méthode dans votre sous-classe, ou paramétrez la variable membre `self.cover_url`avant que cette méthode ne soit appelée.

get_extra_css()[source]

Par défaut renvoie self.extra_css. Outrepassez si vous voulez générer par programme extra_css

get_feeds()[source]

Retourne une liste de flux RSS à récupérer pour ce profil. Chaque élément de la liste doit être un tuple de 2 éléments de la forme (title,url). Si title est None ou une chaîne vide, le titre du flux est utilisé. Cette méthode est utile si votre recette doit faire quelques traitements pour déterminer la liste des flux à télécharger. Si oui, outrepassez dans votre sous-classe.

get_masthead_title()[source]

Outrepasser dans la sous-classe pour utiliser quelque chose d’autre que le titre de la recette

get_masthead_url()[source]

Retourne une URL à l’image masthead pour cette question ou None. Par défaut il retourne la valeur du membre selfl.masthead_url qui est normalement None. Si vous voulez que votre recette télécharge une masthead pour le livre numérique outrepasser cette méthode dans votre sous-classe, ou paramétrer la variable membre `self.masthead_url`avant que cette méthode soit appelée. Les images Masthead sont utilisées dans les fichiers MOBI de Kindle.

get_obfuscated_article(url)[source]

Si vous avez défini articles_are_obfuscated, cette méthode est appelée avec chaque URL d’article. Elle doit retourner le chemin à un fichier sur le système de fichier qui contient l’article HTML. Ce ficher est traité par le moteur de récupération récursif d’HTML, aussi il peut contenir des liens vers des pages/images sur le web. Vous pouvez également renvoyer un dictionnaire de la forme suivante : {“data” : <HTML data>, “url” : <the resolved URL of the article>}. Cela évite de devoir créer des fichiers temporaires. La clé url du dictionnaire est utile si l’URL effective de l’article est différente de l’URL passée dans cette méthode, par exemple, à cause de redirections. Elle peut être omise si l’URL reste inchangée.

Cette méthode est typiquement utile pour les sites qui essayent de rendre difficile l’accès automatique au contenu d’article.

get_url_specific_delay(url)[source]

Renvoie le délai en secondes avant le téléchargement de cette URL. Si vous souhaitez déterminer par programme le délai pour l’URL spécifié, surchargez cette méthode dans votre sous-classe, en renvoyant self.delay par défaut pour les URL que vous ne souhaitez pas affecter.

Renvoie:

Un nombre à virgule flottante, le délai en secondes.

classmethod image_url_processor(baseurl, url)[source]

Effectue un certain traitement sur les URLs d’images (peut-être en supprimant les restrictions de taille pour les images générées dynamiquement, etc.) et renvoie l’URL prétraitée. Retourne None ou une chaîne vide pour ne pas récupérer l’image.

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

Méthode commode qui prend une URL vers la page index et retourne une BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc] de celle-ci.

url_or_raw: Soit une URL soit la page d’index téléchargée comme une chaîne

is_link_wanted(url, tag)[source]

Renvoie True si le lien devrait être suivi ou autrement False. Par défaut, augmente NotImplementedError qui amène le téléchargement à l’ignorer.

Paramètres:

- url – L’URL à suivre

- tag – La balise depuis laquelle l’URL a été dérivée

parse_feeds()[source]

Crée une liste d’articles depuis la liste des flux renvoyés par BasicNewsRecipe.get_feeds(). Renvoie une liste d’objets Feed.

parse_index()[source]

Cette méthode devrait être appliquée dans les recettes qui analysent un site Web au lieu des flix pour produire une liste d’articles. Les utilisations typiques sont pour les sources d’actualités qui ont une page Web « édition imprimable » qui énumère tous les articles dans l’actuelle édition imprimable. Si cette fonction est mise en application, elle sera employée de préférence à meth:BasicNewsRecipe.parse_feeds.

Il doit renvoyer une liste. Chaque élément de la liste doit être un tuple de 2 éléments de la forme ('feed title', list of articles).

Chaque liste d’articles doit contenir des dictionnaires de la forme:

{
'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.
}

Pour un exemple, regardez la recette pour télécharger The Atlantic. En plus, vous pouvez ajouter author pour l’auteur de l’article.

Si vous voulez arrêtez le traitement pour quelque raison et que vous avez calibre qui affiche à l’utilisateur un simple message plutôt qu’une erreur appelez abort_recipe_processing().

populate_article_metadata(article, soup, first)[source]

Appelé quand chaque page HTML appartenant à l’article est téléchargée. Conçu afin d’obtenir les métadonnées de l’article comme auteur/résumé/etc. à partir du HTML analysé (soup).

Paramètres:

- article – Un objet de la classe calibre.web.feeds.Article. Si vous changez le résumé, n’oubliez pas de changer également le text_sumary

- soup – HTML analysé appartenant à cet article

- first – Vrai si et seulement si le HTML filtré est la première page de l’article.

postprocess_book(oeb, opts, log)[source]

Exécutez n’importe quel post-traitement nécessaire sur le livre numérique téléchargé et analysé.

Paramètres:

- oeb – Un objet OEBBook

- opts – Options de conversion

postprocess_html(soup, first_fetch)[source]

Cette méthode est appelée avec la source de chaque fichier HTML téléchargé, après qu’il soit analysé pour des liens et des images. Il peut être employé pour faire arbitrairement un puissant post traitement sur l”HTML. Il devrait renvoyer soup après l’avoir traité.

Paramètres:

- soup – Une instance BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] contenant l”HTML téléchargé.

- first_fetch – Vrai si c’est la première page d’un article.

preprocess_html(soup)[source]

Cette méthode est appelée avec la source de chaque fichier HTML téléchargé, avant qu’il soit analysé pour des liens et des images. Il est appelé après le nettoyage comme spécifié par remove_tags etc. Il peut être employé pour faire arbitrairement un puissant post traitement sur l”HTML. Il devrait renvoyer soup après l’avoir traité.

soup: Une instance BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] contenant l”HTML téléchargé.

preprocess_image(img_data, image_url)[source]

Exécute certains traitements sur les données d’une image téléchargée. Ceci est appliqué sur les données brutes avant que le redimensionnement soit terminé. Dois restituer les données brutes traitées. Restitue Aucun pour sauter l’image.

preprocess_raw_html(raw_html, url)[source]

Cette méthode est appelée avec la source de chaque fichier HTML téléchargé, avant d’être analysé dans un arbre d’objet. raw_html est une chaîne unicode représentant l’HTML brut téléchargé du web. url est l’URL à partir de laquelle l’HTML a été téléchargé.

Notez que cette méthode agit avant preprocess_regexps.

La méthode doit retourner le rw_html traité comme un objet unicode.

classmethod print_version(url)[source]

Prendre une url pointant vers une page web avec du contenu d’article et renvoie l”URL pointant vers la version imprimable de l’article. Par défaut ne fait rien. Par exemple:

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

publication_date()[source]

Utiliser cette méthode pour définir la date à laquelle ce numéro a été publié. La valeur par défaut est le moment du téléchargement. Doit retourner un objet datetime.datetime.

skip_ad_pages(soup)[source]

Cette méthode est appelée avec la source de chaque fichier HTML téléchargé, avant tout les attributs de nettoyage comme remove_tags, keep_only_tags soient appliqués. Notez que preprocess_regexps aura déjà été appliqué. Il est censé permettre à la recette d’ignorer les pages de pub. Si la soup représente une page de pub, renvoie l’HTML de la page réelle. Autrement renvoie None.

soup: Une instance BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] contenant l”HTML téléchargé.

sort_index_by(index, weights)[source]

Méthode commode pour trier les titres dans index selon weights. index est trié sur place. Retourne `index.

index : Une liste de titres.

weights: Un dictionnaire qui mappe les poids aux titres. Si quelques titres dans index n’ont pas de poids, ils sont supposés avoir un poids de 0.

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

Méthode commode pour prendre une Balise BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] et extraire le texte de celle-ci récursivement, incluant n’importe quelles sections CDATA et les attributs alt tag. Renvoie une chaîne Unicode éventuellement vide.

usr_°alt : Si `True`essaye d’utiliser l’attribut alt pour les balises qui n’ont pas de contenu textuel.

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

articles_are_obfuscated = False

Paramétrez à True et implémentez get_obfuscated_article() pour manipuler les sites Web qui essayent de le rendre difficile de ratisser le contenu.

auto_cleanup = False

Extrait automatiquement tout le texte des pages d’articles téléchargés. Utilise les algorithmes du projet lisibilité. Paramétrer ceci à vrai, signifie que vous n’avez pas à vous inquiéter à propos du nettoyage manuel de l’HTML téléchargé (cependant un nettoyage manuel sera toujours supérieur).

auto_cleanup_keep = None

Spécifier les éléments que l’algorithme de nettoyage automatique ne devrait jamais supprimer. La syntaxe est une expression XPath. Par exemple:

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 True la barre de navigation est alignée au centre, autrement elle est alignée à gauche.

compress_news_images = False

Paramétrer ceci à False pour ignorer tous les paramètres de dimensionnement et de compression et pour analyser des images non modifiées. Si True et les autres paramètres de compression sont laissés à leurs valeurs par défaut, des images JPEG seront dimensionnées pour s’adapter aux dimensions d’écran réglées par le profil de sortie et comprimées pour mesurer tout au plus (w * h)/16 où w x h sont les dimensions mesurées d’image.

compress_news_images_auto_size = 16

Le facteur utilisé lors de la compression automatique des images JPEG. Si réglé à None, la compression automatique est désactivée. Autrement, les images seront réduites de taille à (w * h)/compress_news_images_auto_size bytez si possible en réduisant le niveau de qualité, où w x h sont les dimensions de l’image en pixels. La qualité minimale JPEG sera de 5/100 aussi est-il possible que la contrainte ne sera pas rencontrée.Ce paramètre peut être outrepassé par le paramètre compress_news_images_max_size qui procure une taille fixe pour les images. Notez que si vous activez scale_news_images_to_device alors l’image sera d’abord mise à l’échelle et ensuite sa qualité diminuée jusqu’à ce que sa taille soit moins que (w * h)/factor où w et h sont maintenant les dimensions de l’image mise à l’échelle,

compress_news_images_max_size = None

Paramétrer la qualité JPEG afin que les images n’excèdent pas la taille donnée (en KBytes). Si réglé, ce paramètre outrepasse la compression automatique par l’intermédiaire de compress_news_images_auto_size. La qualité minimale JPEG sera de 5/100 ainsi il est possible que cette contrainte ne sera pas rencontrée.

conversion_options = {}

Options spécifiques de recette pour contrôler la conversion du contenu téléchargé dans un livre numérque. Ceci outrepassera n’importe quelle valeur spécifiée par l’utilisateur ou une extension, aussi ne l’utilisr que si absolument nécessaire. Par exemple:

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

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

Par défaut, l’image de couverture renvoyée par get_cover_url() sera utilisée comme couverture du périodique. Remplacer ceci dans votre recette donne l’instruction à calibre de rendre la couverture téléchargée dans un cadre dont la largeur et la hauteur sont exprimées en pourcentage de la couverture téléchargée. cover_margins = (10, 15, “#ffffff”) rembourre la couverture avec une marge blanche de 10px à gauche et à droite, 15px en haut et en bas. Les noms des couleurs sont définis ici [https://www.imagemagick.org/script/color.php]. Notez que pour une raison quelconque, le blanc ne fonctionne pas toujours sous Windows. Utilisez #ffffff à la place

delay = 0

Délai par défaut entre deux téléchargements consécutifs, en secondes. L’argument peut être un nombre à virgule flottante pour indiquer un temps plus précis. Voir get_url_specific_delay() pour implémenter des délais par URL.

description = ''

Quelques lignes qui décrivent le contenu téléchargé par cette recette. Ceci sera employé principalement dans un GUI qui présente une liste de recettes.

encoding = None

Spécifiez un codage de priorité pour les sites qui ont des spécifications incorrectes de jeu de caractère . Le plus commun étant de spécifier latin1 et d’utiliser cp1252. Si aucun, essai pour détecter theencoding. Si None, essaye de détecter l’encodage. S’il est appelable, L’appelable est appelé avec deux arguments : L’objet de recette et la source à décoder. Elle doit renvoyer la source décodée.

extra_css = None

Spécifiez n’importe quel CSS supplémentaire qui devrait être ajouté aux fichiers HTML téléchargés. Il sera inséré dans les balises <style> juste avant la balise de fermeture </head> outrepassant tous les CSS excepté ce qui est déclaré utilisant un attribut de style sur des balises HTML individuelles. Notez que si vous voulez générer par programme extra_css écrasera la méthode get_extra_css() à la place. Par exemple:

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

feeds = None

Liste des flux à télécharger. Peut être soit [url1, url2, ...] ou [('titre1', url1), ('titre2', url2),...]

filter_regexps = []

Liste d’expressions régulières qui détermine quels liens à ignorer. Si vide, elle est ignorée. Utilisé seulement si is_link_wanted n’est pas implémenté. Par exemple:

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

supprimera toutes les URL qui contiennent ads.doubleclick.net.

Seulement une de BasicNewsRecipe.match_regexps ou de BasicNewsRecipe.filter_regexps devrait être définie.

handle_gzip = True

Mettre à False si vous ne voulez pas utiliser les transferts gzippés. Notez que certains vieux serveurs ne fonctionnent plus avec gzip.

ignore_duplicate_articles = None

Ignorer les articles en double qui sont présents dans plus d’une section. Un article en doublon est un article qui a le même titre et/ou URL. Pour ignorer les articles avec le même titre réglez ceci à :

ignore_duplicate_articles = {'title'}

Pour utiliser les URLs à la place réglez le à :

ignore_duplicate_articles = {'url'}

Pour correspondre au titre ou à l’URL, réglez à :

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

Garder uniquement les balises spécifiées et leurs descendants. Pour le format pour spécifier une balise voir BasicNewsRecipe.remove_tags. Si la liste n’est pas vide, alors la balise `<body>`sera vidée et sera remplie avec les balises qui correspondent aux entrées dans la liste. Parc exemple:

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

gardera uniquement les balises qui ont un attribut id de « content » ou de « heading ».

language = 'und'

La langue dans laquelle les actualités sont. Doit être un code ISO-639 soit deux ou trois caractères de long

masthead_url = None

Par défaut, caparlibre emploiera une image défaut pour le masthead (Kindle seulement). Outrepassez ceci dans votre recette pour fournir une URL à utiliser comme masthead.

match_regexps = []

Liste d’expressions régulières qui détermine quels liens à suivre. Si vide, elle est ignorée. Utilisé seulement si is_link_wanted n’est pas implémenté. Par exemple:

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

correspondra à toutes les URL qui contiennent page=some number.

Seulement une de BasicNewsRecipe.match_regexps ou de BasicNewsRecipe.filter_regexps devrait être définie.

max_articles_per_feed = 100

Nombre maximum d’articles à téléchargé de chaque flux. Ceci est principalement utile pour les flux qui n’ont pas de date d’article. Pour plus de flux, vous devriez utiliser BasicNewsRecipe.oldest_article

needs_subscription = False

Si True le GUI demandera à l’utilisateur pour un nom d’utilisateur et un mot de passe à utiliser lors du téléchargement Si réglé à « optionnal » l’utilisation du nom d’utilisateur et du mot de passe devient optionnel

no_stylesheets = False

Indicateur commode pour désactiver le chargement des feuilles de style pour les sites Web qui ont des feuilles de style inappropriées excessivement complexes pour la conversion de formats de livres numériques Si True les feuiiles de style ne sont pas téléchargées et ne sont pas traitées

oldest_article = 7.0

L’article le plus ancien à télécharger de cette source d’actualités. En jours.

preprocess_regexps = []

Liste de règles de substitution term:`regexp à exécuter sur l”HTML téléchargé. Chaque élément de la liste devrait un tuple à deux éléments. Le premier élément du tuple devrait être une expression régulière compilée et la seconde et le second un appelable qui prend un seul objet qui correspond et retourne une chaîne qui remplace la correspondance. Par exemple:

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

supprimera tout de <!–Article ends here–> à </body>.

publication_type = 'unknown'

Le type de publication a placé au journal, au magazine ou au blog. Si réglé à None, aucune métadonnée de type publication ne sera écrite dans le fichier opf.

recipe_disabled = None

Régler à une chaîne non vide pour désactiver la recette. La chaîne sera utilisée comme message de désactivation

recursions = 0

Nombre de niveaux de liens à suivre sur des pages web d’article

remove_attributes = []

Liste des attributs à supprimer de toutes les balises Par exemple:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

Si True les flux vides seront supprimés de la sortie. Cette option n’a pas d’effet si parse_index est outrepassée dans la sous classe. Cela s’indique seulement pour les recettes qui retournent une liste de flux utilisant flux ou get_feeds(). C’est aussi utilisé si vous employez l’option ignore_duplicate_article.

remove_javascript = True

Indicateur commode pour retirer toutes les balises Javascript de l’HTML téléchargé

remove_tags = []

Liste de balises qui doivent être supprimées. Les balises spécifiées sont supprimées de l’HTML téléchargé. Une balise est spécifiée comme un dictionnaire de la forme:

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

Toutes les clés sont optionnelles. Pour une explication complète du critère de recherche, voir Beautiful Soup <https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree> __ Un exemple commun:

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

Ceci supprimera toutes les balises <div class= »advert »> et tous leurs descendants de l”HTML téléchargé.

remove_tags_after = None

Supprimer toutes les balises qui surviennent après la balise spécifiée. Pour le format pour spécifier une balise voir BasicNewsRecipe.remove_tags. Par exemple:

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

supprimera toutes les balises après le premier élément avec id= »content ».

remove_tags_before = None

Supprimer toutes les balises qui surviennent avant la balise spécifiée. Pour le format pour spécifier une balise voir BasicNewsRecipe.remove_tags. Par exemple:

remove_tags_before = dict(id='content')

supprimera toutes les balises avant le premier élément avec id= »content ».

requires_version = (0, 6, 0)

Version minimale de calibre pour utiliser cette recette

resolve_internal_links = False

Si réglé à True alors les liens dans les articles téléchargés qui pointent vers d’autres articles téléchargés sont changés pour pointer vers la copie téléchargée de l’article plutôt que son URL web originale. Si vous réglez à True, vous aurez aussi besoin d’implémenter canonicalize_internal_url() pour travailler avec le schéma d’URL de votre site web particulier.

reverse_article_order = False

Renversez l’ordre des articles dans chaque flux

scale_news_images = None

Dimensions maximales (h,l) pour mettre les images à l’échelle. Si scale_news_images_to_device est True est réglé aux dimensions de l’écran du périphérique sauf s’il n’y a pas de profil paramétré, dans quel cas elle est laissé à n’importe quelle valeur qui lui a été assignée (par défaut None)

scale_news_images_to_device = True

Remet les images à l’échelle pour s’ajuster aux dimensions de l’écran du périphérique paramétré par le profil de sortie ? Ignoré si aucun profil de sortie n’a été paramétré.

simultaneous_downloads = 5

Nombre de téléchargements simultanés. Réglé à 1 si le serveur est difficile. Automatiquement réduit à 1 si BasicNewsRecipe.delay > 0

summary_length = 500

Nombre de caractères maximum dans la description courte

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    '

Le CSS qui est utilisé pour styliser les modèles, c-à-d les barres de navigation et les Tables des Matières. Plutôt qu’outrepasser cette variable, vous devriez utiliser extra_css dans votre recette pour personnaliser l’apparence.

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

Le format de chaîne pour la date affichée sur la première page. Par défaut : Day_Name, Day_Number Month_Name Year

timeout = 120.0

Délai en secondes pour récupérer les fichiers depuis le serveur

title = 'Source d’Actualités Inconnue'

Le titre à utiliser pour le livre numérique

use_embedded_content = None

Normalement nous essayons de deviner si un flux a des articles complets inclus basé sur la longueur du contenu inclus. Si None, alors l’estimation par défaut est employée. Si True alors nous supposons toujours que les flux ont du contenu inclus et si False nous assumons toujours que le flux n’a pas de contenu inclus.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- La visionneuse de livre numérique

La visionneuse de livre numérique
calibre inclut une Visionneuse de livre numérique qui peut déchiffrer tous les principaux formats de livre numérique. La Visionneuse de livre numérique est hautement personnalisable et possède de nombreuses fonctionnalités avancées.

- Démarrer la Visionneuse de livre numérique

- Naviguer dans un livre numérique

- Mise en évidence du texte

- Lire à voix haute

- Recherche dans le texte

- Suivre les liens en utilisant uniquement le clavier

- Personnalisation de l’aspect et de la convivialité de votre expérience de lecture

- Consultation du dictionnaire

- Copier du texte et des images

- Zoom sur les images

- Raccourcis clavier

- Contenu non fluide

- Concevoir votre livre pour qu’il fonctionne bien avec la visionneuse de calibre

Démarrer la Visionneuse de livre numérique

Vous pouvez consulter n’importe lequel des livres de votre bibliothèque calibre en sélectionnant le livre et en appuyant sur le bouton Visualiser”. Cela ouvrira le livre dans la visionneuse de livres numériques. Vous pouvez également lancer a visionneuse de livres numériques à partir du menu Démarrer de Windows. Sous macOS, vous pouvez l’épingler sur le dock et le lancer à partir de là. Sous Linux, vous pouvez utiliser son lanceur dans les menus du bureau ou exécuter la commande :command:`ebook-viewer.

Naviguer dans un livre numérique

Vous pouvez « tourner les pages » d’un livre soit :

- En cliquant dans la marge gauche ou droite de la page avec la souris

- En appuyant sur les touches Barre d'espace, page haut, page bas ou les touches fléchées

- Sur un écran tactile en tapotant sur le texte ou en glissant à gauche et à droite

Vous pouvez accéder aux commandes de la visionneuse soit :

- En faisant un clic droit sur le texte

- En appuyant sur les touches Echap ou Menu

- Sur un écran tactile en tapotant sur le tiers supérieur de l’écran

La visionneuse a deux modes, « paginé » et « continu ». En mode paginé, le contenu du livre est présenté sous forme de pages, comme un livre papier. En mode continu, le texte est présenté en continu, comme dans un navigateur Web. Vous pouvez passer de l’un à l’autre en utilisant les Préférences de la visionneuse sous Mise en page ou en appuyant sur la touche Ctrl+M.

Signets

Lorsque vous êtes au milieu d’un livre et fermez la visionneuse, elle se souviendra où vous avez arrêté la lecture et y retournera la prochaine fois que vous ouvrirez le livre. Vous pouvez également définir des signets dans le livre en utilisant le bouton Signet ou en pressant Ctrl+B dans la visionneuse de livre numérique. Lors de l’affichage des livres au format EPUB, ces signets sont enregistrés dans le fichier EPUB lui-même. Vous pouvez ajouter des signets, puis envoyer le fichier à un ami. Quand il ouvrira le fichier, il sera en mesure de voir vos signets. Vous pouvez désactiver ce comportement dans la section Divers des préférences de la visionneuse.

Table des Matières

Si le livre que vous lisez a une Table des Matières définie, vous pouvez y accéder en appuyant sur le bouton Table des Matières. Cela fera apparaître une liste des sections du livre. Vous pouvez cliquer sur n’importe laquelle de celles-ci pour accéder à cette partie du livre.

Navigation par emplacement

Les livres numériques, contrairement aux livres papier, n’ont aucune notion des pages. Vous pouvez vous référer à des emplacements précis dans les livres numériques en utilisant la fonctionnalité Aller à → Emplacement dans les contrôles de visualisation.

Vous pouvez utiliser cette information d’emplacement pour désigner sans ambiguïté une partie du livre quand vous en discutez avec des amis ou pour y faire référence dans d’autres œuvres. Vous pouvez entrer ces emplacements sous Aller à → Emplacement dans les contrôles de la visionneuse.

Ici ce trouve une URL que vous pouvez copier vers le presse-papiers et coller dans d’autres programmes ou documents. En cliquant sur cette URL vous ouvrirez le livre dans la visionneuse de livre numérique de calibre à l’emplacement actuel.

Si vous cliquez sur les liens à l’intérieur du livre numérique, vous serez amenés vers différentes parties du livre, vers les notes de fin, par exemple. Vous pouvez utiliser le boutons Précédent et Suivant dans le coin supérieur gauche des contrôles de la visionneuse. Ces boutons se comportent exactement comme ceux d’un navigateur internet.

Mode référence
calibre possède également un Mode référence très pratique. Vous pouvez l’activer en cliquant sur le bouton Mode référence dans les commandes de la visionneuse. Une fois que vous avez fait cela, chaque paragraphe aura un numéro unique affiché au début, composé des numéros de section et de paragraphe.

Vous pouvez utiliser ce nombre pour vous référer sans ambiguïté à certaines parties des livres lorsque vous en discutez avec des amis ou que vous y faites référence dans d’autres ouvrages. Vous pouvez entrer ces nombres dans la fonction Aller à pour naviguer vers un endroit de référence particulier.

Mise en évidence du texte

Lorsque vous sélectionnez du texte dans la visionneuse, une petite barre contextuelle apparaît à côté de la sélection. Vous pouvez cliquer sur le bouton de mise en évidence dans cette barre pour créer une mise en évidence. Vous pouvez ajouter des notes et modifier la couleur de la surbrillance. Sur un écran tactile, appuyez longuement sur un mot pour le sélectionner et afficher la barre contextuelle. Une fois en mode surbrillance, vous pouvez modifier le texte sélectionné à l’aide de poignées de sélection adaptées aux écrans tactiles. Faites glisser les poignées vers les marges supérieures ou inférieures pour faire défiler le texte tout en le sélectionnant. Vous pouvez également Maj+clic ou clic droit pour étendre la sélection, ce qui est particulièrement utile pour les sélections sur plusieurs pages.

Vous pouvez utiliser le bouton Mises en évidence dans les commandes de la visionneuse pour afficher un panneau séparé avec une liste de toutes les mises en évidence du livre, triée par chapitre.

Vous pouvez parcourir toutes les mises en évidence de votre bibliothèque calibre en cliquant avec le bouton droit sur le bouton Voir et en choisissant Parcourir les annotations.

Enfin, si vous utilisez le Serveur de contenu calibre Content dans le visionneuse du navigateur, vous pouvez faire en sorte que le visionneuse synchronise ses annotations avec le visionneuse du navigateur en allant sur Préférences → Divers dans les préférences de la visionneuse et en entrant le nom d’utilisateur du Serveur de Contenu de la visionneuse avec lequel il doit se synchroniser. Utilisez la valeur spéciale « * » pour la synchronisation avec les utilisateurs anonymes.

Lire à voix haute

La visionneuse peut lire le texte du livre à voix haute. Pour l’utiliser, il suffit de cliquer sur le bouton Lire à voix haute dans les commandes de la visionneuse pour commencer à lire le texte du livre à voix haute. Le mot en cours de lecture est mis en évidence. La parole est synthétisée à partir du texte en utilisant les services de votre système d’exploitation pour le text-to-speech. Vous pouvez changer la voix utilisée en cliquant sur l’icône de l’engrenage dans la barre qui s’affiche lorsque Lire à voix haute est actif.

Vous pouvez également lire à haute voix les passages surlignés en ajoutant le bouton Lire à voix haute à la barre de sélection dans les préférences de la visionneuse sous Comportement de sélection.

Note

La prise en charge de la synthèse vocale dans les navigateurs est très incomplète et truffée de bogues, de sorte que la qualité de Lire à voix haute dans le navigateur dépend de la qualité de la prise en charge de la synthèse vocale dans le navigateur sous-jacent. En particulier, le surlignage du mot courant ne fonctionne pas, et le changement de vitesse ou de voix fera que la lecture recommencera depuis le début.

Note

Sous Linux, Lire à voix haute nécessite que Speech Dispatcher [https://freebsoft.org/speechd] soit installé et fonctionnel.

Note

Sous Windows, toutes les voix installées peuvent ne pas être visibles par le sous-système SAPI utilisé pour le text-to-speech. Il existe des instructions pour rendre toutes les voix visibles <https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108>`_.

Recherche dans le texte

La visionneuse dispose de capacités de recherche très puissantes. Appuyez sur la touche Ctrl+F ou accédez aux commandes de la visionneuse et cliquez sur rechercher. La forme la plus simple de recherche consiste à rechercher le texte que vous entrez dans la zone de texte. Les différentes formes de recherche sont choisies par la boîte de mode de recherche située sous l’entrée de recherche. Les modes disponibles sont les suivants :

1. Contains - Le mode par défaut le plus simple. Le texte saisi dans le champ de recherche est recherché partout. La ponctuation, les accents et les espaces sont ignorés. Par exemple, la recherche : Pena correspondra à tout ce qui suit : pénal, pen a, pen.a et Peña. Si vous sélectionnez la case Sensible à la casse, les accents, les espaces et la ponctuation ne sont plus ignorés.

2. Mots entiers - Recherche des mots entiers. Ainsi, par exemple, la recherche pena correspondra au mot Peña mais pas au mot Pénal. Comme pour les recherches Contains ci-dessus, les accents et la ponctuation sont ignorés, sauf si la case Sensible à la casse est cochée.

3. Mots proches - Recherche des mots entiers qui sont proches les uns des autres. Ainsi, par exemple, la recherche calibre cool correspondra aux endroits où les mots calibre et cool apparaissent à moins de soixante caractères l’un de l’autre. Pour modifier le nombre de caractères, ajoutez le nouveau nombre à la fin de la liste de mots. Par exemple, « calibre cool awesome 120 » correspondra aux endroits où les trois mots se trouvent à moins de 120 caractères l’un de l’autre. Notez que la ponctuation et les accents ne sont pas ignorés pour ces recherches.

4. Regex - Interprète le texte de recherche comme une expression régulière. Pour en savoir plus sur l’utilisation des expressions régulières, consultez le tutoriel.

Suivre les liens en utilisant uniquement le clavier

La visionneuse de livres numériques dispose d’un Mode Indices” qui vous permet de cliquer sur les liens dans le texte sans utiliser la souris. Appuyez sur la touche :kbd:`Alt+f et tous les liens de l’écran en cours seront mis en évidence par un chiffre ou une lettre. Appuyez sur la lettre de votre clavier pour cliquer sur le lien.En appuyant sur la touche Echap, vous annulerez le Mode indices sans sélectionner aucun lien.

Si plus de trente-cinq liens sont affichés à l’écran, certains d’entre eux comporteront plusieurs lettres, auquel cas tapez la première et la deuxième, ou la première et appuyez sur Entrer pour les activer. Vous pouvez également utiliser la touche Retour arrière pour annuler une erreur de frappe.

Personnalisation de l’aspect et de la convivialité de votre expérience de lecture

Vous pouvez changer la taille des polices à la volée en utilisant Taille de police dans les contrôles de la visionneuse ou Ctrl++ ou Ctrl+- ou en maintenant la touche :kbd:`Ctrl” et en utilisant la molette de la souris.

Les couleurs peuvent être changées dans la section :guilabel:`Couleurs” des préférences de la visionneuse.

Vous pouvez modifier le nombre de pages affichées à l’écran ainsi que les marges de page dans :guilabel:`Mise en page” dans les préférences de la visionneuse.

Vous pouvez afficher des en-têtes et pieds de page personnalisés tels que le temps restant à lire, le titre du chapitre en cours, la position dans le livre, etc. via la section En-têtes et pieds de page des préférences de la visionneuse.

Une personnalisation plus avancée peut être réalisée par les réglages Styles. Ici, vous pouvez spécifier une image d’arrière-plan à afficher sous le texte ainsi qu’une feuille de style que vous pouvez définir et qui sera appliquée à chaque livre. En l’utilisant, vous pouvez faire des choses comme changer les styles de paragraphe, la justification du texte, etc. Pour des exemples de feuilles de style personnalisées utilisées par les utilisateurs de calibre, voir les forums [https://www.mobileread.com/forums/showthread.php?t=51500].

Consultation du dictionnaire

You can look up the meaning of words in the current book by double clicking or long tapping the word you want to lookup and then clicking the lookup button that looks like a library.

Copier du texte et des images

Vous pouvez sélectionner du texte et des images avec votre souris puis faire un clic droit et sélectionner Copier pour copier dans le presse-papiers. Le matériel copié peut être collé dans une autre application sous forme de texte et d’images.

Zoom sur les images

Vous pouvez zoomer pour afficher une image en taille réelle dans une fenêtre séparée en double-cliquant ou en appuyant longuement sur celle-ci. Vous pouvez également faire un clic droit sur l’image et choisir Voir l’image.

Raccourcis clavier

La visionneuse dispose de nombreux raccourcis clavier, comme le reste de calibre. Ils peuvent être personnalisés dans la visionneuse Preferences. Les raccourcis par défaut sont listés ci-dessous :

Raccourcis clavier pour la visionneuse de calibre

Raccourci clavier

Action

Home, Ctrl+ArrowUp, Ctrl+ArrowLeft

Défiler jusqu’au début du fichier actuel dans un livre à plusieurs fichiers

Ctrl+Home

Faire défiler jusqu’au début du livre

Ctrl+End

Faire défiler jusqu’à la fin du livre

End, Ctrl+ArrowDown, Ctrl+ArrowRight

Défiler jusqu’a la fin du fichier actuel dans un livre à plusieurs fichiers

ArrowUp

Défilement vers l’arrière, en douceur en mode flux et par écran plein en mode paginé

ArrowDown

Défilement vers l’avant, en douceur en mode flux et par écran plein en mode paginé

ArrowLeft

Défiler un peu vers la gauche en mode flux et d’une page en mode paginé

ArrowRight

Défiler un peu vers la droite en mode flux et d’une page en mode paginé

PageUp, Shift+Spacebar

Faire défiler vers l’arrière par écrans pleins.

PageDown, Spacebar

Faire défiler vers l’avant par écrans pleins.

Ctrl+PageUp

Faire défiler vers la section précédente

Ctrl+PageDown

Faire défiler vers la section suivante

Alt+ArrowLeft

Reculer

Alt+ArrowRight

Avancer

Ctrl+T

Afficher/masquer la Table des Matières

Ctrl+S

Lire à voix haute

Alt+P

Modifiez rapidement les paramètres en créant et en utilisant les Profiles

Alt+f

Suivre les liens avec le clavier

Ctrl+C

Copier dans le presse-papier

Alt+C

Copier l’emplacement actuel dans le presse-papiers

Ctrl+Shift+C

Copier l’emplacement actuel comme URL calibre:// dans le presse-papiers

/, Ctrl+f, Cmd+f

Lancer la recherche

F3, Enter

Trouver le suivant

Shift+F3, Shift+Enter

Trouver le précédent

Ctrl+Plus, Meta+Plus

Augmenter la taille de la police

Ctrl+Minus, Meta+Minus

Diminuer la taille de la police

Ctrl+0

Restaurer la taille de police par défaut

Ctrl+]

Augmenter le nombre de pages par écran

Ctrl+[

Diminuer le nombre de pages par écran

Ctrl+Alt+C

Rendre automatique le nombre de pages par écran

F11, Ctrl+Shift+F

Passer en mode plein écran

Ctrl+M

Basculer entre le mode paginé et le mode continu pour la mise en page du texte

Ctrl+W

Basculer la barre de défilement

Ctrl+X

Activer le mode référence

Ctrl+B

Afficher/masquer les signets

Ctrl+Alt+B

Nouveau signet

Ctrl+N, Ctrl+E

Afficher les métadonnées du livre

Ctrl+Alt+F5, Ctrl+Alt+R

Recharger le livre

Ctrl+Shift+ArrowRight

Modifier la sélection actuelle d’un mot

Ctrl+Shift+ArrowLeft

Modifier la sélection actuelle d’un mot

Shift+ArrowRight

Modifier la sélection actuelle en avançant d’un caractère

Shift+ArrowLeft

Modifier la sélection actuelle en reculant d’un caractère

Shift+ArrowDown

Modifier la sélection actuelle en avançant d’une ligne

Shift+Home

Étendre la sélection actuelle au début de la ligne

Shift+End

Étendre la sélection actuelle à la fin de la ligne

Ctrl+A

Sélectionner tout

Shift+ArrowUp

Modifier la sélection actuelle en reculant d’une ligne

Ctrl+Shift+ArrowDown

Modifier la sélection actuelle en avançant d’un paragraphe

Ctrl+Shift+ArrowUp

Modifier la sélection actuelle en reculant d’un paragraphe

Esc, MenuKey

Afficher les contrôles de la Visionneuse de livre numérique

Ctrl+Comma, Ctrl+Esc, Meta+Esc, Meta+Comma

Afficher les préférences de la Visionneuse de livre numérique

Ctrl+G, ;, :

Aller à un emplacement ou une position spécifique du livre

Ctrl+Spacebar

Basculer vers le défilement automatique

Alt+ArrowUp

Défilement automatique plus rapide

Alt+ArrowDown

Défilement automatique plus lent

Ctrl+I

Afficher/masquer l’Inspecteur

Ctrl+L

Afficher/masquer le panneau de recherche de mot

Ctrl+Q (Cmd+Q on macOS)

Quitter

Ctrl+P

Imprimer le livre au format PDF

Ctrl+F11

Basculer la barre d’outils

Ctrl+H

Basculer le panneau des mises en évidence

Ctrl+D

Éditer ce livre

Contenu non fluide

Certains livres ont un contenu tellement grand qu’ils ne peuvent pas être divisés aux limites de la page. Par exemple tables ou balises <pre>. Dans ce cas, vous devez passer en mode flow en appuyant sur Ctrl+M pour lire ce contenu. Alternativement, vous pouvez également ajouter le CSS suivant à la section Styles des préférences de la visionneuse pour forcer le visionneuse à décomposer des lignes de texte en balises <pre>

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

Concevoir votre livre pour qu’il fonctionne bien avec la visionneuse de calibre

La visionneuse calibre définira la classe « is-calibre-viewer » sur l’élément racine. Vous pouvez donc écrire des règles CSS qui ne s’appliquent qu’à cet élément. En outre, la visionneuse définira les classes suivantes sur l’élément body :

body.calibre-viewer-dark-colors

Défini lors de l’utilisation d’un schéma de couleur sombre

body.calibre-viewer-light-colors

Défini lors de l’utilisation d’un schéma de couleurs claires

body.calibre-viewer-paginated

Défini en mode pagination

body.calibre-viewer-scrolling

Défini en mode flux (non paginé)

body.calibre-footnote-container

Défini lors de l’affichage d’un popup d’une note de bas de page

Pour finir, vous pouvez utiliser les couleurs du schéma de couleurs de calibre via les variables CSS [https://developer.mozilla.org/fr/docs/Web/CSS/Using_CSS_custom_properties]. La visionneuse calibre définit les variables suivantes : --calibre-viewer-background-color, --calibre-viewer-foreground-color et éventuellement --calibre-viewer-link-color dans les thèmes de couleurs qui définissent une couleur de lien.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Conversion de livre numérique

Conversion de livre numérique
calibre a un système de conversion qui est prévu pour être très simple d’utilisation. Normalement, il suffit d’ajouter un livre à calibre, cliquer sur « convertir » et calibre s’efforcera de générer un résultat aussi proche que possible de l’original. calibre accepte un très large panel de formats, certains étant plus appropriés que d’autres à la conversion en livre numérique. Dans le cas de ces formats, ou si vous souhaitez simplement avoir plus de contrôle sur le système de conversion, calibre a beaucoup d’options pour affiner le processus de conversion. Cependant, il faut noter que le système de conversion de calibre ne peut pas remplacer un programme d’édition de livre numérique. Pour éditer des livres numériques, je recommande de les convertir d’abord en EPUB ou AZW3 en utilisant calibre et ensuite, utiliser la fonction Éditer le livre pour les perfectionner. Vous pouvez alors utiliser le livre numérique édité comme base pour la conversion vers d’autres formats avec calibre.

Ce document fera principalement référence aux paramètres de conversion comme trouvé dans la boîte de dialogue de conversion, représentée ci-dessous. Tous ces paramètres sont aussi disponible par l’interface en invite de commande pour la conversion, documenté dans ebook-convert. Dans calibre, vous pouvez obtenir de l’aide sur n’importe quel paramètre individuel en passant par-dessus avec votre souris, une info-bulle décrivant le paramètre apparaîtra.

Contenu

- Introduction

- Apparence

- Mise en page

- Traitement heuristique

- Rechercher & remplacer

- Détection de la structure

- Table des matières

- Utiliser des images comme titre de chapitre en convertissant des documents sources en HTML.

- Utiliser les attributs de balise pour le texte des entrées dans la Table des Matières

- Comment les options sont définies/sauvegardées pour la conversion

- Conseil spécifique au format

Introduction

La première chose à comprendre à propos du système de conversion, c’est qu’il fonctionne comme un pipeline. Schématiquement, cela ressemble à ceci :

Le format d’entrée est tout d’abord converti en XHTML par l”Extension d’Entrée appropriée. Ce fichier HTML est alors transformé. En dernier lieu, le fichier XHTML traité est converti vers le format spécifié par l”Extension de Sortie appropriée. Les résultats de la conversion peuvent grandement varier, en fonction du format entrant. Certains formats se convertissent bien mieux que d’autres. Une liste des meilleures sources de format pour la conversion peut être trouvée ici.

C’est dans les manipulations qui s’opèrent sur la sortie XHTML que tout le travail se fait. Il existe de nombreuses manipulations, par exemple, l’insertion des métadonnées en tant que première page du livre électronique, la détection des titres de chapitres pour la création d’une Table des Matières, l’ajustement des tailles des polices, et cetera. Il est important de se souvenir que toutes les manipulations se font sur le fichier XHTML par l” Extension d’entrée, pas sur le fichier de base lui-même. Donc, par exemple, si vous demandez à calibre de convertir un fichier RTF vers de l’EPUB, il convertira d’abord en XHTML en interne, les multiples manipulations seront appliquées au fichier XHTML et ensuite l” Extension de sortie créera le fichier EPUB, générant automatiquement toutes les métadonnées, la Table des Matières, et cetera.

Vous pouvez voir ce procédé en action en utilisant l’option de débogage . Spécifiez juste le chemin vers un dossier pour le fichier de débogage. Pendant la conversion, calibre placera le fichier XHTML généré aux différentes étapes du pipeline de conversion dans différents sous-dossier. Les quatre sous-dossiers sont :

Étapes du pipeline de conversion

Dossier

Description

entrée

Ceci contient le fichier HTML fr sortie créé par l’Extension d’origine. Utilisez le pour déboguer l’Extension d’entrée.

analysé

Le résultat du prétraitement du fichier et de sa conversion en XHTML par l’Extension d’entrée. Utiliser ceci pour déboguer la détection de structures.

structure

Après la détection de structure, mais avant l’aplanissement du CSS et la conversion des tailles de polices. Utiliser pour déboguer la conversion de la taille des polices et les transformations CSS.

traité

Juste avant que le livre numérique ne soit passé à l’extension de sortie. Utiliser ceci pour déboguer l’extension de sortie.

Si vous souhaitez légèrement éditer le document d’entrée avant que calibre ne le convertisse, la meilleure chose à faire est d’éditer les fichiers dans le sous-dossier Input, puis de les archiver, et d’utiliser l’archive ZIP en tant que fichier d’entrée pour les conversions suivantes. Pour ceci, utiliser la boîte de dialogue Édition des méta informations pour ajouter l’archive ZIP en tant que format pour le livre et ensuite, dans le coin supérieur gauche de la boîte de dialogue de conversion, sélectionner ZIP comme le format d’entrée.

Ce document s’occupera principalement des différentes manipulations qui agiront sur le fichier XHTML intermédiaire et sur comment les contrôler. À la fin se trouve des astuces pour chaque format d’entrée/de sortie.

Apparence

Contenu

- Polices

- Texte

- Mise en page

- Esthétique

- Transformer les styles

- Transformer du HTML

Ce groupe d’options contrôle de nombreux aspects de l’apparence et de la présentation du livre numérique converti.

Polices

Une des meilleures particularités lors de l’expérience de lecture électronique est la possibilité de facilement réajuster la taille des polices pour s’adapter aux besoins personnels et conditions d’éclairage. calibre a des algorithmes sophistiqués pour s’assurer que tous les livres ont une taille de police cohérente, quelle que soit la taille de police spécifiée dans le fichier d’entrée

La taille de police de base est la taille de police la plus utilisée dans ce document, càd, la taille de la masse de texte dans le document. Quand vous spécifiez un Taille de police de base, calibre redimensionne automatiquement toutes les tailles de police proportionnellement, pour que la taille de police la plus utilisée devienne la taille spécifiée et que les autres tailles se dimensionnent adéquatement. En choisissant une taille plus large, vous pouvez augmenter la taille des polices du document, et vice versa. Quand vous fixez la taille de police de base, il vaut mieux aussi fixer la clef de taille de police .

Normalement, calibre choisira automatiquement une taille de police de base appropriée au profil de sortie que vous avez choisi (voir Mise en page). Toutefois, vous pouvez outrepassez ceci si le choix par défaut ne vous convient pas.

L’option Clef de taille de police vous permet de contrôler comment la taille des polices qui ne sont pas celles de base sont redimensionnées. L’algorithme de redimensionnement des polices utilise une clef de taille de police, qui est simplement une liste séparée par des virgules des tailles de police. La clef des tailles de police donne à calibre combien de fois plus grosse ou plus petite une police devrait être par rapport à la police de base. L’idée est qu’il devrait y avoir une quantité limitée de tailles de police dans un document. Par exemple, une taille pour le corps de texte, quelques tailles pour les différents niveaux de titres et quelques tailles pour les indices, exposants et pied de pages. La clef de taille de police permettra à calibre de compartimenter les tailles de police dans le document entrant en différentes « boîtes » correspondantes aux différentes tailles de police logique.

Illustrons avec une exemple. Supposons que le document source que l’on convertit soit produit par quelqu’un qui possède une excellente acuité visuelle et utilise une taille de police de base de 8pt. Ce qui veut dire que la masse de texte du document est à 8pt, alors que les titres sont légèrement plus large (disons 10 et 12pt) et les pied de pages un peu plus petit à 6pt. Maintenant, si nous utilisons les paramètres suivant

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

Le document de sortie aura une taille de police de base de 12pt, des titres de 14pt et 16pt et des pieds de pages de 8pt. Supposons maintenant que nous voulions que les plus gros titres ressortent mieux et que les pieds de pages soient aussi légèrement plus larges. Pour arriver à ce résultat, la clef de taille de police devrait être changée en

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

Le plus gros titres feront maintenant 18pt, alors que les pieds de pages feront 9pt. Vous pouvez jouer avec ces options pour essayer de trouver quels seraient les conditions optimale pour vous en utilisant l’assistant de redimensionnement des polices, qui peut être accédé en cliquant sur le petit bouton à coté du paramètre Clef de taille de police.

Tous les redimensionnements de taille de police peuvent aussi être désactivé ici, si vous désirez conserver les tailles de police du document original.

Un paramètre lié est Hauteur de ligne. Hauteur de ligne contrôle la longueur verticale des lignes. Par défaut, (une hauteur de ligne de 0), aucune manipulation n’est effectuée sur la hauteur des lignes. Si vous spécifiez une valeur différente de 0, la hauteur des lignes sera fixée à tous les endroits qui ne spécifient pas leur propre hauteur de ligne. Toutefois, cet outil est « brutal » et devrait être utilisé avec modération. Si vous voulez ajuster la hauteur des ligne de certaines section du fichier original, il vaut mieux utiliser le Extra-CSS.

Dans cette section, vous pouvez également dire à calibre d’incorporer toutes les polices référencées dans le livre. Cela permettra aux polices de fonctionner sur les liseuses même si elles ne sont pas disponibles sur le périphérique.

Texte

Le texte peut être justifié ou non. Le texte justifié comporte des espaces supplémentaires entre les mots pour donner une marge droite lisse. Certains préfèrent le texte justifié, d’autres non. Normalement, calibre conserve la justification dans le document original. Si vous voulez l’écraser, vous pouvez utiliser l’option Justification du texte dans cette section.

Vous pouvez également indiquer le calibre d”Améliorer la ponctuation qui remplacera les guillemets, tirets et ellipses par leurs alternatives typographiquement correctes. Notez que cet algorithme n’est pas parfait et qu’il vaut la peine d’examiner les résultats. L’inverse, à savoir, Simplifier la ponctuation est également disponible.

Enfin, il y a Encodage des caractères d’entrée. De vieux documents peuvent parfois ne pas spécifier leur encodage de caractère. Une fois convertis, cela peut entraîner une corruption des caractères ne faisant pas partie du français ou des caractères spéciaux comme les guillemets intelligents. calibre essaye de détecter automatiquement l’encodage de caractère du document source, mais n’y arrive pas toujours. Vous pouvez le forcer à supposer un encodage de caractère particulier en utilisant ce paramètre. cp1252 est un encodage classique pour les documents qui ont été réalisé par des programmes utilisant Windows. Vous devriez aussi lire Comment puis-je convertir mon fichier contenant des caractères non-anglais, ou des guillemets intelligents ? pour plus d’information sur les problèmes d’encodage.

Mise en page

Normalement, les paragraphes d’un fichier XHTML sont rendus avec un saut de ligne et sans retrait de texte. calibre a plusieurs paramètres qui permettent de contrôler cela. Retirer l’espace entre les paragraphes s’assure qu’aucun des paragraphes n’ont d’espace entre eux. Il fixe aussi le retrait de texte à 1.5em (cela peut être changé) pour marquer le début de chaque paragraphe. Insère une ligne vide fait l’opposé, garantissant qu’il y ait bien un saut de ligne entre chaque paragraphe. Chacun de ces paramètres sont complet, retirant les espaces, ou en insérant pour « tous » les paragraphes (techniquement, les balises <p> et <div>). Ceci est fait afin que vous puissiez régler le paramètre et être sûr qu’il fonctionne comme annoncé, quel que soit l’état du fichier original. La seule exception est lorsque le fichier d’origine utilise des sauts de ligne poussifs pour augmenter l’espace entre les paragraphes

Si vous souhaitez retirer l’espace entre tous les paragraphes à part une petite partie, n’utilisez pas ces options. Ajouter ces lignes de CSS au fichier Extra-CSS:

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

Ensuite, dans le document source, marquer les paragraphes qui nécessite un espacement avec class= »spacious ». Si votre document original n’est pas en HTML, utiliser l’option de Débogage, décrite dans l’Introduction pour obtenir de l’HTML (utiliser le sous dossier input).

Une autre option utile est Transformer les tableaux en texte. Certains documents mal conçus utilisent les tableaux pour contrôler l’affichage du texte dans la page. Convertis, ces documents ont souvent le texte qui déborde de la page et d’autres dysfonctionnements. Cette option extraira le contenu des tableaux et le présentera dans un mode linéaire. Noter que cette option linéarise tous les tableaux, à utiliser uniquement si vous êtes sûr que le document source n’utilise pas de tableaux à des fins appropriées, comme la présentation de données.

Esthétique

L’option CSS supplémentaire vous permet de spécifier du CSS quelconque qui sera appliqué à tous les fichiers HTML dans la source. Ce CSS est appliqué en priorité et devrait donc outrepasser la plupart du CSS présent dans le document source lui-même. Vous pouvez utiliser ce paramètre pour mettre au point finement la présentation/disposition de votre document. Par exemple, si vous voulez que tous les paragraphes de class endnote soient alignés sur la droite, ajoutez juste

.endnote { text-align: right }

ou si vous souhaitez changer la découpure de chaque paragraphe

p { text-indent: 5mm; }

Extra CSS est une option très puissant, mais il est nécessaire de comprendre comment le CSS fonctionne pour pouvoir l’utiliser à son plein potentiel. Vous pouvez utiliser l’option pipeline de débogage ci-dessus pour voir quel CSS est présent dans votre document d’origine.

Une option plus simple est d’utiliser Filtrer l’information de style. Ceci vous permet de supprimer toutes les propriétés CSS, des types spécifiés, du document. Par exemple, vous pouvez l’utiliser pour supprimer toutes les couleurs ou polices.

Transformer les styles

Il s’agit de l’instrument le plus puissant en matière de style. Vous pouvez l’utiliser pour définir des règles qui modifient les styles en fonction de diverses conditions. Par exemple, vous pouvez l’utiliser pour changer toutes les couleurs vertes en bleu, ou supprimer tous les styles gras du texte ou colorier tous les titres d’une certaine couleur, etc.

Transformer du HTML

Similaire aux transformations de styles, mais vous permet d’apporter des modifications au contenu HTML du livre. Vous pouvez remplacer une balise par une autre, ajouter des classes ou d’autres attributs aux balises en fonction de leur contenu, etc.

Mise en page

Les options de Mise en page sont faites pour contrôler la disposition de l’écran, comme les marges et les tailles d’écran. Il comprend des options permettant de régler les marges des pages, qui seront utilisées par l’extension de sortie si le format de sortie supporte les marges. De plus, il vous faudra décider d’un profil d’entrée et d’un profil de Sortie. Chaque ensemble de profil gère comment interpréter les mesures dans les documents d’entrée/de sortie, les tailles d’écrans et les clefs de redimensionnement de police par défaut.

Si vous savez que le fichier que vous convertissez était destiné à être utilisé sur un dispositif/une plate-forme logicielle particulière, choisissez le profil d’entrée correspondant, sinon choisissez simplement le profil d’entrée par défaut. Si vous savez que les fichiers que vous produisez sont destinés à un type d’appareil particulier, choisissez le profil de sortie correspondant. Sinon, choisissez l’un des profils de sortie génériques. Si vous convertissez en MOBI ou AZW3, vous voudrez presque toujours choisir l’un des profils de sortie Kindle. Sinon, votre meilleur choix pour les appareils modernes de lecture de livres électroniques est de choisir le profil de sortie Generic e-ink HD.

Le profil de Sortie contrôle aussi la taille de l’écran. Ceci induira, par exemple, le redimensionnement automatique des images afin qu’elle tienne à l’écran pour certains formats de sortie. Choisissez donc un profil de périphérique qui a une taille proche de votre taille d’écran.

Traitement heuristique

Le traitement heuristique offre une variété de fonctions qui peuvent être utilisée pour détecter et corriger des problèmes communs dans des fichiers source médiocrement formatés. Utilisez cette option si votre document source souffre d’un formatage médiocre. Vu que ces fonctions reposent sur des modèles communs, sachez que dans certain cas le résultat peut s’avérer pire, à utiliser avec précaution donc. Par exemple, plusieurs de ces options retireront tous les espaces qui ne sont pas insécable, ou pourrait inclure des faux positifs dépendant de la fonction.

Activer le traitement heuristique

Cette option active l’étape Traitement heuristique du pipeline de conversion de calibre. Elle doit être activée pour que différentes sous-fonctions s’appliquent.

Redéfinir les lignes

Activer cette option forcera calibre à essayer de détecter et corriger les sauts de ligne brutaux qui existe au sein du document en utilisant des indices dans la ponctuation et la longueur des lignes. calibre essayera d’abord de déterminer si des sauts de ligne brutaux existent, et s’ils n’en apparaissent aucun calibre n’essayera pas de redéfinir les lignes. Le facteur de redéfinition des lignes peut-être réduit sir vous souhaitez « forcer » calibre à redéfinir les lignes

Facteur de redéfinition des lignes

Cette option contrôle l’algorithme utilisé par calibre pour supprimer les ruptures de ligne. Par exemple, si la valeur de cette option est de 0,4, cela signifie que calibre supprimera les sauts de ligne à la fin des lignes dont la longueur est inférieure de 40 % de toutes les lignes du document. Si votre document ne comporte que quelques sauts de ligne qui doivent être corrigés, cette valeur doit être réduite à une valeur comprise entre 0,1 et 0,2.

Détecter et mettre en forme les titres de chapitre et les sous-titres sans mise en forme

Si votre document n’a pas de format différent pour les titres et en-têtes de chapitre par rapport au reste du texte, calibre peut utiliser cette option pour essayer de les détecter et de les entourer d’une balise de titre. Des balises <h2> sont utilisées pour les en-têtes de chapitres; Des balises <h3> sont utilisées pour tout titre détectés.

Cette fonction ne créera pas une TdM, mais dans de nombreux cas, cela permettra à la détection des chapitres par défaut de calibre de détecter les chapitres correctement et de construire une TdM. Ajuster l’XPath sous Détection de la Structure si une TdM n’est pas créée automatiquement. S’il n’y a aucun autre titre utilisé dans le document, alors régler Détection de Structure à « //h:h2 » sera le moyen le plus simple de créer une TdM pour votre document.

Les titres insérés ne sont pas formaté, pour appliquer un format, utiliser l’option Extra CSS sous le paramètre Apparence et Présentation de conversion. Par exemple, pour centrer les titres, utiliser ceci

h2, h3 { text-align: center }

Renuméroter les séquences de balises <h1> ou <h2>

Certains formats de publication utilise de multiple balises <h1> ou <h2> successivement pour leurs titres. Les paramètres de conversion par défaut de calibre couperont ces titres en 2 parties. Cette option renumérotera les balises de titre pour éviter cette scission.

Supprime les lignes blanches entre les paragraphes

Cette option permet à calibre d’analyser les lignes vides incluses dans le document. Si chaque paragraphe est entrecoupé d’une ligne blanche, calibre supprimera tous ces paragraphes blancs. Les séquences de plusieurs lignes vides seront considérées comme des sauts de scène et conservées comme un seul paragraphe. Cette option diffère de l’option Supprimer l’espacement des paragraphes sous Apparence et présentation” Cette option peut également supprimer les paragraphes qui ont été insérés à l’aide de l’option :guilabel:`Insérer une ligne vide de calibre.

Garantir une mise en forme cohérente des séparations entre deux scènes

Grâce à cette option, calibre essayera de détecter des marqueurs de ruptures de scènes commun et s’assurera qu’ils sont alignés au centre. Des marqueurs de rupture de scène « doux », càd les rupture de scène qui ne sont définies que par des espaces blanc additionel, sont créer pour s’assurer qu’ils ne s’affichent pas de pairs avec de saut de ligne.

Remplacer les symboles de séparation de scène

Si cette option est configurée alors calibre remplacera les marqueurs de rupture de scène qu’il trouve avec le texte spécifié par l’utilisateur. Noter que certains caractères ornementaux peuvent ne pas être supportés sur tous les périphériques.

En général il vaudrait mieux éviter d’utiliser des balises HTML, calibre écartera toutes les balises et utilisera un balisage pré-défini. Les balises <hr />, càd les règles horizontales, et les balises <img> font exceptions. Les règles horizontales peuvent optionnellement être spécifiées dans les styles, si vous choisissez d’ajouter votre propre style pour vous assurer qu’il comprend le paramètre “largeur”, sinon les informations de style seront écartées. Les balises d’images peuvent être utilisées, mais calibre ne permet pas d’ajouter des images durant la conversion, cela doit être faire après coup en utilisant la fonctionnalité “Éditer le livre”.

Exemple de balise d’image (placer l’image dans un dossier “Images” dans l’EPUB après conversion):

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

Exemple de règle horizontale avec styles:

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

Enlever les traits d’union superflus

calibre analysera tout le contenu présentant des traits d’union dans le document lorsque cette option est activée. Le document lui-même est utilisé en tant que base d’analyse. Cela permet à calibre de retirer avec précision les traits d’unions pour n’importe quel mot du document quelque soit la langue, ainsi que pour les mots inventés et les mots scientifiques. Le problème principal est que les mots n’apparaissant qu’une fois dans le document ne seront pas modifiés. L’analyse s’effectue en deux passages, le premier analyse les fins de lignes. Les lignes sont redéfinies uniquement si le mot existe avec ou sans trait d’union dans le document. Le second passage analyse tous les mots comprenant des traits d’union dans le document, les traits d’unions sont retirés si les mots existent autre part dans le document sans tiret.

Mettre en italique les mots et les schémas usuels

Si active, calibre cherchera des mots et modèles qui dénote de l’italique et les mettra en italique. Par exemple les conventions habituelles de texte comme « mot » ou les phrases qui devrait normalement être en italique, par exemple les phrases latines comme “etc” ou “et cetera”

Remplacer les mises en retrait par des retraits CSS

Certains documents prennent comme convention d’utiliser des espaces insécables pour les retraits de texte. Lorsque cette option est activée calibre tentera de détecter ce type de formatage et les convertira à un retrait de texte de 3% en utilisant le CSS.

Rechercher & remplacer

Ces options sont principalement utiles pour la conversion de documents PDF ou OCR, mais elles peuvent aussi être utilisée pour arranger beaucoup de documents avec des problèmes spécifiques. Par exemple, certaines conversion laissent des en-têtes ou des pieds de page dans le texte. Ces options utilisent des expressions régulière pour essayer de détecter en-têtes, pied de page et autres textes quelconques pour les retirer ou les remplacer. Gardez en tête qu’ils opèrent sur le fichier XHTML intermédiaire produit par le pipeline de conversion. Il existe un assistant pour vous aidez à customiser les expressions habituelles dans votre document. Cliquez sur la baguette magique sous le la boîte de dialogue, et puis sur le bouton “Test” après avoir entrer votre expression de recherche. Les résultats probant seront surlignés en Jaune

La recherche fonctionne en utilisant une expression habituelle en Python. Tout le texte identifié sera retiré ou remplacer par le modèle de remplacement. Le modèle de remplacement est optionnel, si laissé blanc alors le texte correspondant à la recherche sera supprimé du document. Vous pouvez en apprendre plus sur les expressions régulières et leur syntaxe dans Tout à propos de l’utilisation des expressions régulières dans calibre.

Détection de la structure

La détection de structure pousse calibre à essayer de détecter au mieux les éléments structuraux dans le document original, quand ils ne sont pas spécifiés correctement. Par exemple, les chapitres, saut de page, en-tête, pied de page, etc. Comme vous pouvez l’imaginer, ce processus change grandement d’un livre à l’autre. Heureusement, calibre a de puissantes options pour le contrôler. Avec la puissance vient la complexité, mais une fois que vous avez pris le temps de comprendre cette complexité, vous vous rendrez compe que votre effort en valait le coup.

Chapitres et sauts de page
calibre a deux ensemble d’options pour détection des chapitres et insertion des sauts de page. Cela peut parfois être légèrement déboussolant, car par défaut, calibre insérera automatiquement un saut de page après un chapitre détecté mais aussi après les endroits perçu par l’option de détection de saut de page. La raison est qu’il existe de nombreux emplacements où des sauts de page devrait être inséré mais qui ne sont pas des limites de chapitres. Aussi, les chapitres détecté peuvent optionnellement être inséré dans la Table des Matières générée automatiquement.

calibre utilise XPath, un langage puissant pour permettre à l’utilisateur de spécifier les limites de chapitres/sauts de page. XPath peut sembler un peu décourageant au premier abord, mais heureusement, il y a le tutoriel XPath dans le Manuel de l’Utilisateur. Gardez en tête que la Détection de la structure s’opère sur le fichier XHML intermédiaire produit par le pipeline de conversion. Utilisez l’option de débogage décrite dans Introduction pour comprendre les paramètres appropriés pour votre livre. Il existe aussi un bouton pour un assistant XPath pour vous aider à générer des expressions XPath simples.

Par défaut, calibre utilise les expressions suivantes pour la détection de chapitre:

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

Cette expression est assez complexe, car elle essaye de gérer un certain nombre de cas généraux simultanément. Ce qui veut dire que calibre considérera que les chapitres commencent soit à des balises <h1> ou <h2> qui ont les mots (chapitre, livre, section ou partie) en elles ou qui ont l’attribut class= »chapter ».

Une option apparentée est Marque de chapitre, qui vous permet de contrôler ce que calibre fait lorsqu’il détecte des chapitres. Par défaut, il insérera un saut de page avant le chapitre. Vous pouvez aussi insérez une ligne à la place, ou en plus du saut de page. Il peut aussi ne rien faire.

Le paramètre par défaut pour détecter les sauts de page est

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

ce qui veut dire que calibre insérera un saut de page avant chaque balise <h1> et <h2> par défaut.

Note

Les expressions par défaut peuvent changer en fonction du format d’origine que vous convertissez.

Divers

Il y a quelque options de plus dans cette section.

Insérer les métadonnées sur une page au début du livre

Une des choses pratique avec calibre c’est qu’il vous permet d’entretenir des métadonnées très complètes sur tous vos livres, par exemple un classement, les étiquettes, les commentaires, etc. Cette option créera une page unique avec toutes les métadonnées et l’insérera dans le livre numérique converti, habituellement juste après la couverture. Pensez-y pour créer votre propre jaquette personnalisée.

Retirer la première image

Parfois, le document source que vous convertissez inclue la couverture dans le livre, au lieu de la séparer. Si vous spécifiez aussi une couverture dans calibre, alors le livre converti en aura deux. Cette option enlèvera simplement la première image du document source, s’assurant ainsi que le livre converti à uniquement une couverture, celle spécifiée dans calibre.

Table des matières

Quand le document source a une Table des Matières dans ses métadonnées, calibre l’utilisera simplement. Cependant, soit certain vieux formats ne supportent pas les Table des Matières intégrée dans les métadonnées, soit certains livres n’en ont tout simplement pas. Dans ces cas là, les options dans cette section peuvent vous aider à générer automatiquement une Table des Matières dans le livre numérique converti, basée sur le contenu réel du document source.

Note

L’utilisation de ces options peut être un peu difficile pour un résultat parfait. Si vous préférez créer/éditer la Table des Matières à la main, convertissez en EPUB ou en AZW3 et sélectionner la case en bas de la section Table des Matières dans la boite de dialogue de conversion intitulé Affiner la Table des matière manuellement après conversion. Ceci lancera l’éditeur de TdM après la conversion. Cela permet de créer des entrées dans la Table des Matières simplement en cliquant dans le livre à l’endroit où vous voulez que l’entrée pointe. Vous pouvez aussi utiliser l’éditeur de TdM seul, sans conversion. Allez à Préférences → Interface → Barre d’Outils et ajouter Éditeur de TdM à la barre d’outil principale. Sélectionner alors simplement le livre que vous souhaitez éditer et cliquez sur le bouton Éditeur de TdM.

La première option est Utilisation forcée de la Table des Matières générée automatiquement. En cochant cette option vous pouvez forcer calibre à outrepasser toutes les Table des Matières qu’il trouve dans les métadonnées du document source et à en générer une.

La manière de fonctionner par défaut de la création de Table des Matières automatique est que, calibre essayera d’abord d’ajouter tout chapitres détectés à la table des matières générée. Vous pouvez apprendre comment personnaliser la détection des chapitres dans la section Détection de la structure ci-dessus. Si vous ne souhaitez pas inclure les chapitres détectés dans la table des matières générée, coché l’option Ne pas ajouter les chapitres détectés.

Si moins de chapitres que Seuil de chapitre sont détecté, calibre ajoutera alors tout hyperlien qu’il trouve dans le document à la Table des Matières. Cela fonctionne souvent bien car beaucoup de document source incluent dans la Table des Matières en lien hypertexte. L’option Nombre de lien peut être utilisée pour contrôler ce comportement. Si fixé à zéro, aucun lien ne sera ajouté. Si fixé à un nombre plus grand que zéro, le nombre de chapitre ajouté sera maximum de ce nombre.

calibre filtrera automatiquement les doublons dans la Table des Matières générée. Cependant, s’il y a d’autres entrées indésirable, vous pouvez les filtrer en utilisant l’option Filtre TdM. Ceci est une expression régulière qui fera correspondre le titre des entrées dans la table des matières générée. Dès qu’une correspondance est trouvée, elle sera supprimée. Par exemple, pour retirer toutes les entrée « Next » ou « Previous » utiliser

Next|Previous

L’option TdM niveau 1,2,3 vous permet de créer une Table des Matières sophistiquée à plusieurs niveaux. Ce sont des expressions XPath qui correspondent à des balises dans le fichier XHTML intermédiaire produit par le pipeline de conversion. Voir Introduction sur comment avoir accès à ce fichier XHTML. Lire aussi le Tutoriel XPath, pour apprendre comment construire des expressions XPath. À côté de chacune des options il y a un bouton qui lance un assistant pour aider à la création d’expressions XPath basiques. Les exemples suivant illustreront comment utiliser ces options.

Supposons que vous avez un document source qui donne un fichier XHTML qui ressemble à ceci :

<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>

Alors, nous fixons les options à

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

Ceci résultera en une Table des Matières générée automatiquement à deux niveaux qui ressemblera à

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

Avertissement

Les formats de sortie ne supportent pas tous les Tables des Matières à plusieurs niveaux. Il vaut mieux d’abord essayer en format EPUB. Si cela fonctionne, alors essayez votre format de préférence.

Utiliser des images comme titre de chapitre en convertissant des documents sources en HTML.

Supposons que vous souhaitiez utiliser une image comme titre de chapitre, mais vous souhaitez toujours que calibre soit capable de générer automatiquement une Table des Matières pour vous à partir des titres des chapitres. Utilisez le balisage HTML suivant pour faire cela :

<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>

Fixer le paramètre TdM à 1 niveau à //h:h2`. Ensuite, pour le chapitre deux, calibre prendra le titre à partir de la valeur de l'attribut ``title sur la balise <h2>, vu que la balise ne contient aucun texte.

Utiliser les attributs de balise pour le texte des entrées dans la Table des Matières

Si vous avez des chapitres particulièrement long et que vous souhaitez des versions raccourcies dans la Table des Matières, vous pouvez utiliser les attributs de titre pour atteindre cela, par exemple :

<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>

Fixer le paramètre TdM à 1 niveau à //h:h2/@title. Alors calibre prendra le titre dans la valeur de l’attribut title sur les balises <h2>, au lieu d’utiliser le texte dans la balise. Noter le /@title restant dans l’expression XPath, vous pouvez utiliser ceci pour dire à calibre de prendre le texte de n’importe quel attribut.

Comment les options sont définies/sauvegardées pour la conversion

Il existe deux endroits où les options de conversions peuvent être fixées dans calibre. La première est dans Préférences->Conversion. Ces paramètres sont ceux par défaut pour les options de conversions. Dès que vous essayerez de convertir un nouveau livre, les paramètres fixés ici seront utilisés par défaut.

Vous pouvez aussi changer les paramètres dans la boite de dialogue de conversion à chaque conversion de livre. Quand vous convertissez un livre, calibre se souvient des paramètres utilisés pour ce livre, et si vous souhaitez le convertir à nouveau, les paramètres sauvés pour ce livre auront priorité sur les paramètres par défaut fixés dans Préférences. Vous pouvez ramener les paramètres individuels à ceux par défaut en utilisant le bouton Restaurer par défaut dans la boite de dialogue de conversion du livre susmentionné. Vous pouvez retirer des paramètres sauvegardés pour un groupe de livre en sélectionnant tous les livres et en cliquant sur le bouton Édition des métadonnées, pour ouvrir la boîte de dialogue de l’éditeur de métadonnées multiples, il existe une option pour retirer des paramètres de conversion sauvegardés au pied de cette fenêtre

Lorsque vous convertissez par lot un ensemble de livres, les paramètres sont pris dans l’ordre suivant (le dernier l’emporte):

- De paramètres par défaut fixés dans Préférences->Conversion

- Des paramètres de conversion sauvé pour chaque livre converti (s’ils existent). Ceci peut-être désactivé par l’option dans le coin supérieur gauche de la boîte de dialogue de Conversion par Lot.

- Des paramètres fixés dans la boîte de dialogue de la Conversion par Lot

Notez que les paramètres finaux pour chaque livre dans une Conversion par Lot sera sauvegardé et réutilisé si ce livre est à nouveau converti. Vu que la priorité la plus haute dans la Conversion par Lot est donnée aux paramètres de la Conversion par Lot, ceux-ci surpasseront tous les paramètres spécifiques de livre. Convertissez donc uniquement par lot des livres qui nécessitent des paramètres similaires. Les exceptions sont les métadonnées et les paramètres de formats entrant spécifique. Vu que la boîte de dialogue de Conversion par Lot n’a pas de paramètres pour ces deux catégories, ils seront pris des paramètres spécifiques du livre (s’ils existent) ou de ceux par défaut.

Note

Vous pouvez voir quels sont les paramètres qui ont été utilisés durant n’importe quelle conversion en cliquant sur l’icone tournante dans le coin inférieur droit, puis en double cliquant sur la tache de conversion individuelle. Ceci fera apparaître un journal de conversion qui contiendra les paramètres qui ont effectivement été utilisé, en haut.

Conseil spécifique au format

Vous trouverez ici des conseils spécifique quant à la conversion de formats particulier. Les options spécifiques à chaque format, d’entrée ou de sortie, sont disponibles dans le dialogue de conversion sous leur propre section, par exemple Entrée TXT ou Sortie EPUB.

Convertir des documents Mircosoft Word
calibre peut convertir automatiquement des fichiers .docx créer par Microsoft Word 2007 et plus récent. Ajoutez simplement le fichier dans calibre et cliquez sur convertir.

Note

Il y a un fichier .docx de démonstration [https://calibre-ebook.com/downloads/demos/demo.docx] qui illustrent les capacités du moteur de conversion de calibre. Téléchargez-le et convertissez-le en EPUB ou en AZW3 pour voir ce que calibre peut faire.

calibre générera automatiquement une Table des Matières à partir des titres si vous marquez vos titres avec les styles Titre 1, Titre 2, etc. dans Microsoft Word. Ouvrez le livre numérique sorti dans la Visionneuse de livre numérique de calibre et cliquez sur le bouton Table des Matières pour voir la Table des Matières générée.

Des fichiers .doc plus ancien

Pour des fichiers .doc plus ancien, vous pouvez sauver le document en HTML avec Microsoft Word et ensuite convertir le fichier HTML qui en résulte avec calibre. En sauvant en HTML, soyez sûr d’utiliser l’option « Sauvegarder comme une page Web, Filtrée » car cela produira un HTML propre qui se convertira facilement. Notez que Word produit de très mauvais fichier HTML, la conversion peut prendre beaucoup de temps, soyez donc patient. Si vous possédez une version plus récente de Word, vous pouvez aussi le sauver directement en .docx.

Une autre alternative est d’utiliser le logiciel gratuit LibreOffice. Ouvrez votre fichier .doc dans LibreOffice et enregistrez-le au format .docx, qui peut être directement converti dans calibre.

Convertir des documents TXT

Les documents TXT n’ont aucun moyen bien définis de spécifié le formatage de texte comme le gras, l’italique, etc., ou la structure du document comme les paragraphes, les titres, les sections et ainsi de suite, mais il y une variété de conventions qui sont habituellement utilisées. Par défaut, calibre tente de détecter automatiquement le formatage et le balisage adéquat en fonction de ces conventions.

L’entrée TXT comporte un certain nombre d’option pour différencié comment les paramètres sont détectés.

Style de paragraphe: Auto

Analyse le fichier texte et essaye de déterminer automatiquement comment les paragraphes sont définis. Cette option fonctionne généralement bien, si les résultats obtenus ne sont pas bon, essayez l’une des options manuelles.>

Style de paragraphe : Bloc

Considère que les limites de paragraphes sont un saut de ligne ou plus

This is the first.

This is the
second paragraph.

Style de paragraphe: Unique

Considère que chaque ligne est un paragraphe:

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

Style de paragraphe: Imprimé

Considère que chaque paragraphe commence avec un retrait de texte (soit un tab ou plus de 2 espaces). Les paragraphes finissent quand la prochaine ligne commençant avec un retrait est atteinte.

This is the
first.
This is the second.

This is the
third.

Style de paragraphe: Non formaté

Considère que le document n’a pas de formatage, mais utilise des sauts de lignes « dûr ». La ponctuation et des lignes de demi-longueur sont utilisée pour recréer les paragraphes.

Style de formatage: Automatique

Essaye de détecter le type de balisage de formatage. Si aucun balisage n’est utilisé alors le formatage heuristique sera utilisé.

Style de formatage: Heuristique

Analyse le document pour des en-têtes de chapitres communs, des ruptures de scènes, et des mots en italique et applique le balisage HTML approprié durant la conversion.

Style de formatage: Markdown

calibre supporte également la syntaxe Markdown pour les entrées TXT. Le Markdown permet d’ajouter un formatage basique au document TXT, comme du gras, de l’italique, des en-têtes de section, des tableaux, des listes, une Table des Matières, etc. Marquer le titre des chapitres avec un # (p. ex. : # Mon titre) et paramétrer l’expression XPath de détection des chapitres à « //h:h1 » est la manière la plus simple de générer une bonne Table des Matières à partir d’un document TXT. Vous pouvez en apprendre plus sur la syntaxe Markdown ici : daringfireball [https://daringfireball.net/projects/markdown/syntax].

Style de formatage: Aucun

N’applique aucun formatage spécial au texte, le document est converti en HTML sans aucun changement.

Conversion de documents PDF

Les documents PDF sont les pires formats à partir desquels on peut convertir. C’est un format à taille de page et placement de texte fixe. Ce qui veut dire qu’il est très difficile de déterminer où un paragraphe commence et l’autre finit. calibre essayera de déballer mes paragraphes en utilisant un Facteur de déballage de ligne. Ceci est une échelle utilisée pour déterminer la longueur à laquelle les lignes devraient être déballée. Les valeurs valide sont des décimaux se trouvant entre 0 et 1. LA valeur par défaut est 0.45, juste en dessous de la moitié de la longueur d’une ligne. Descendez cette valeur si vous souhaitez inclure plus de texte au déballage. Augmentez pour en inclure moins. Vous pouvez ajuster cette valeur dans les paramètres de conversion sous guilabel:Entrée PDF.

Aussi, ils ont souvent des en-têtes et des pieds de page qui font partie du document et qui seront inclus dans le texte. Utilisez l’option Rechercher et remplacer pour retirer les en-têtes et pieds de pages pour réduire ce problème. Si les en-têtes et pieds de page ne sont pas supprimés cela peu endommager le déballage des paragraphes. Pour apprendre à utiliser les options de suppression d’en-tête et de pieds de page, lire Tout à propos de l’utilisation des expressions régulières dans calibre.

Quelques limitations des entrée PDF sont:

- Les documents complexe, à multiple colonne et basé sur des images ne sont pas supportés

- L’extraction d’image vectorielle et de tableau à partir du document ne sont pas supportées non plus.

- Certains PDF utilisent des glyphes spéciaux pour représenter ll ou ff ou fi, etc. La conversion ce ceux-ci pourrait ne pas marcher simplement en fonction de comment ils sont représenté à l’intérieur du PDF.

- Les liens et les Tables des Matières ne sont pas supportés

- Les PDFs qui utilisent des polices non-Unicode intégrée pour représenter des caractères qui ne font pas partie du français engendreront une sortie tronquée pour ces caractères

- Certain PDF sont fait de photographies de page avec du texte reconnu par OCR derrière elles. Dans ces cas là, calibre utilise le texte venant de l’OCR, qui peut être fort différent de ce que vous voyez dans le fichier PDF.

- Les PDFs utilisés pour afficher du texte complexe, comme des langages lu de droite à gauche et les composition mathématiques ne se convertiront pas bien

Pour répéter le PDF est un vraiment, vraiment mauvais format à utilisé comme source. Si vous devez absolument utiliser du PDF, alors soyez prêt à trouver un fichier de sortie se trouvant n’importe où entre décent et inutilisable, dépendant du PDF source.

Collections de Bandes Dessinées

Une collection de bandes dessinée est un fichier .cbc. Un fichier .cbc est un ZIP qui contient d’autres fichiers CBZ/CBR. Le fichier .cbc doit aussi contenir un simple fichier texte appelé comics.txt, encodé en UTF-8. Le fichier comics.txt doit contenir une liste des bandes dessinées contenue dans le fichier .cbc, sous la forme nom_du_fichier:titre, comme illustré ci-dessous:

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

Le fichier .cbc contiendra alors:

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

calibre convertira automatiquement ce fichier .cbc en un livre numérique avec une Table des Matières pointant vers chaque entrée dans comics.txt

Démonstration de formatage d’EPUB avancée

Divers formatages avancés pour les fichiers EPUB sont expliqués dans ce fichier de démonstration [https://calibre-ebook.com/downloads/demos/demo.epub]. Le fichier a été créé à partir d’HTML codé à la main en utilisant calibre et est destiné à être utilisé comme modèle pour vos efforts de création d’EPUB.

L’HTML source à partir duquel il a été créé est disponible ici demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. Les paramètres utilisés pour créer l’EPUB à partir du ZIP sont

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

Notez que vu que ce fichier explore le potentiel de l’EPUB, la plupart des formatages avancés ne fonctionneront pas sur des lecteurs moins capable que le lecteur intégré de calibre.

Convertir des documents ODT
calibre peut convertir directement les fichiers ODT (OpenDocument Text). Vous devriez utilisé les styles pour formater votre document et minimiser l’utilisation de formatage direct. En insérant des images dans votre document, vous devez les ancrer aux paragraphes, les images qui sont ancrées aux pages finiront au début de la conversion.

Pour permettre la détection automatique des chapitres, vous devez les marquer avec les styles intégrés appelés Heading 1, Heading 2, …, Heading 6 (Heading 1 équivaut à la balise HTML <h1>, Heading 2 à <h2>, etc). Lorsque vous convertissez dans calibre, vous pouvez indiquer le style que vous avez utilisé dans la case Détecter les chapitres à. Exemple :

- Si vous marquez les chapitres avec le style Heading 2, vous devez définir la case « Détecter les chapitres à » sur //h:h2`.

- Pour une table des matières imbriquée avec des sections marquées par Heading 2 et des chapitres marqués par Heading 3, vous devez entrer //h:h2|//h:h3. Sur la page Convertir - Table des matières, mettez la case Niveau 1 de la Table des matières à `//h:h2` et la case Niveau 2 de la Table des matières à `//h:h3`.

Les propriétés de document bien connues (Titre, Mots-clefs, Description, Créateur) sont reconnues et calibre utilisera la première image (pas trop petite, et avec de bonnes proportions) comme image de couverture.

Il y a aussi un mode de propriété avancé de conversion qui est activé en définissant la propriété personnalisée opf.metadata (de type “Oui ou Non”) à Oui dans votre document ODT (Fichier->Propriétés->Propriétés personnalisées). Si cette propriété est détectée par calibre, les propriétés personnalisées suivantes sont reconnues (opf.authors remplace le créateur du document):

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

De plus, vous pouvez spécifier l’image à utiliser comme couverture en la nommant opf.cover (clic droit, Image->Options->Nom) dans le fichier ODT. Si aucune image de ce nom n’est trouvée, la méthode «intelligente» est utilisée. La détection de la couverture pourrait entraîner des couvertures en double dans certains formats de sortie, le processus supprimera le paragraphe (uniquement si le seul contenu est la couverture!) du document. Mais cela ne fonctionne que avec l’image nommée!

Pour désactiver la détection de couverture vous pouvez définir la propriété personnalisée opf.nocover (de type “Oui ou Non”) à Oui en mode avancé.

Convertir en PDF

Le premier paramètre, le plus important, à décider lors de la conversion en PDF est la taille de la page. Par défaut, calibre utilise une taille de page « U.S. Letter ». Vous pouvez changer ceci vers un autre standard de taille de page ou une taille complètement personnalisée dans la section Sortie PDF de la boîte de dialogue de conversion. Si vous générez un PDF à utiliser sur un périphérique spécifique, vous pouvez activer, à la place, l’option pour utiliser la taille de page à partir de Profil de sortie de l’onglet Mise en page de la boîte de dialogue de conversion. De cette façon, si votre profil de sortie est paramétré sur Kindle, calibre créera un PDF avec une taille de page adaptée pour la lecture sur le petit écran Kindle.

En-têtes et pieds de page

Vous pouvez ajouter des en-têtes et pieds de page quelconques sur chaque page du PDF en spécifiant des modèles d’en-tête et de pied de page. Les modèles sont simplement des extraits de code HTML qui sont interprétés dans les régions d’en-tête et de pied de page. Par exemple, pour afficher le numéro des pages centré au bas de chaque page, en vert, utilisé le modèle de pied de page suivant:

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

calibre remplacera automatiquement _PAGENUM_ avec le numéro de page actuel. Vous pouvez même afficher du contenu différent sur les pages paires et impaires, par exemple, le modèle d’en-tête suivant affiche le titre sur les pages impaires et l’auteur sur les pages paires:

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

calibre remplacera automatiquement _TITLE_ et _AUTHOR_ avec le titre et l’auteur du document converti. Régler justify-content sur flex-end fera en sorte que le texte soit aligné à droite.

Vous pouvez aussi afficher du texte aux bords gauche et droit et changer la taille des polices, comme montré dans ce modèle d’en-tête:

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

Ceci affichera le titre à gauche et l’auteur à droite, dans une taille de police plus petite que le texte principal.

Vous pouvez aussi utiliser la section actuelles dans les modèles, comme montré ci-dessous

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

_SECTION_ est remplacé par le nom de la section actuelle, quelle qu’il soit. Ces noms sont pris dans la métadonnée Table des Matières dans le document (le PDF sortant). Si le document n’a pas de table des matières, alors il sera remplacé par du texte vide. Si une page PDF unique à plusieurs sections, la première section de la page sera utilisée. Pareillement, il y a une variable nommée _TOP_LEVEL_SECTION_ qui peut être utilisée pour obtenir le nom de la section supérieure actuelle.

Vous pouvez même utiliser du javascript dans les modèles d’en-tête et de pied de page, par exemple, le modèle suivant fera commencer les numéros de page à 4 au lieu de 1:

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

En outre, il existe d’autres variables que vous pouvez utiliser dans les en-têtes et les pieds de page, documentées ci-dessous :

- _TOTAL_PAGES_ - nombre total de pages dans le fichier PDF, utile pour mettre en place un compteur de progrès, par exemple.

- _TOP_LEVEL_SECTION_PAGES_ - nombre total de pages dans la section haute en cours d’utilisation

- _TOP_LEVEL_SECTION_PAGENUM_ - le numéro de page de la page en cours dans la section haute en cours d’utilisation

Note

Lorsque vous ajoutez des en-têtes et des pieds de page, assurez vous que les valeurs des marges de haut et bas de page soit assez large, sous la section PDF Output dans la boite de dialogue de conversion.

Table des Matières imprimable

Vous pouvez aussi ajouter une Table des Matières imprimable à la fin du PDF qui liste le numéro de page de chaque section. Ceci est très pratique si vous comptez imprimer le PDF sur papier. Si vous souhaitez utiliser le PDF sur un support électronique, alors l’Aperçu PDF permet cette fonctionnalité et est généré par défaut.

Vous pouvez personnaliser l’aspect de la Table des Matières générée en utilisant le paramètre de conversion Extra CSS sous Apparence dans la boite de dialogue de conversion. Le CSS utilisé par défaut est listé ci-dessous, copier le simplement et appliquer tous les changements que vous souhaitez.

.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 }

Marges de page personnalisées pour les fichiers HTML individuels

Si vous convertissez un EPUB ou AZW3 avec plusieurs fichiers HTML individuels à l’intérieur et que vous souhaitez modifier les marges de page pour un fichier HTML particulier, vous pouvez ajouter le bloc de style suivant au fichier HTML en utilisant l’éditeur de livre numérique de calibre :

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

Ensuite, dans la section sortie PDF de la boîte de dialogue de conversion, activez l’option Utiliser les marges de page du document à convertir. Maintenant toutes les pages générées à partir de ce fichier HTML auront des marges 10pt.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Éditer des livres numériques

Éditer des livres numériques
calibre a un éditeur de livre numérique intégré qui peut être utilisé pour éditer des livres aux formats EPUB et AZW3 (Kindle). L’éditeur vous montre l’HTML et le CSS qui sont utilisés à l’intérieur des fichiers livre, avec une prévisualisation directe qui se met à jour au fur et à mesure que vous faites des changements. Il contient aussi divers outils automatisés pour effectuer un nettoyage usuel et des tâches de réparation.

Vous pouvez utiliser cet éditeur en faisant un clic droit sur n’importe quel livre dans calibre et en sélectionnant guilabel:Éditer le livre.

Contenu

Fonctionnement de base

L’Explorateur de fichiers

- Renommer des fichiers

- Fusionner des fichiers

- Ordre de fichier texte changeant

- Marquage de la couverture

- Suppression des fichiers

- Exportation de fichiers

- Ajout de nouvelles images/polices/etc. ou création de nouveaux fichiers vides

- Remplacement des fichiers

- Lier des feuilles de style aux fichiers HTML efficacement

Rechercher & remplacer

- Recherches sauvegardées

- Mode fonction

- Recherche ignorant les balises HTML

Outils automatisés

- Edition de la Table des Matières

- Vérification du livre

- Ajout d’une couverture

- Intégration des polices référencées

- Créer un sous-ensemble de polices

- Amélioration de la ponctuation

- Transformation des propriétés CSS

- Suppression en cours des règles CSS inutilisées

- Correction de l’HTML

- Embellissement des fichiers

- Insertion d’une Table des Matières

- Configuration de Sémantique

- Filtrage des informations de style

- Mise à jour des éléments du livre

Points de Contrôle

Le Panneau de prévisualisation directe

- Fractionnement des fichiers HTML

Le panneau CCS en temps réel

Outils divers

- La vue Table des Matières

- Vérification de l’orthographe des mots dans le livre

- Insérer des caractères spéciaux

- La vue inspecteur de code

- Vérifications des liens externes

- Téléchargement de ressources externes

- Organisation des fichiers par type dans des dossiers

- Importation de fichiers dans d’autres formats de livres numériques comme l’EPUB

- L’outil Rapports

Fonctionnalités spéciales dans l’éditeur de code

- Mise en évidence de la syntaxe

- Aide sensible au contexte

- Auto-complétion

- Fragments

Fonctionnement de base

Note

Une présentation vidéo de l’éditeur de livre numérique de calibre est disponible ici [https://calibre-ebook.com/fr/demo#tutorials].

Quand vous ouvrez un livre la première fois avec l’outil d’Édition de livre, vous serez en présence d’une liste de fichiers sur la gauche. Ceux-ci sont les fichiers HTML individuels, les feuilles de style, les images, etc. qui forment le contenu du livre. Double cliquez simplement sur un fichier pour démarrer son édition. Notez que si vous voulez faire quelque chose de plus sophistiqué que de faire quelques petits ajustements, vous aurez besoin de connaître le Tutoriel HTML [http://html.net/tutorials/html/] et le Tutoriel CSS [http://html.net/tutorials/css/].

Au fur et à mesure que vous faites des changements au HTML ou au CSS dans l’éditeur, les changements seront prévisualisés, en direct, dans le panneau de prévisualisation à gauche. Quand vous êtes content avec l’apparence des changements que vous avez fait, cliquez sur le bouton Enregistrer ou utilisez Fichier → Enregistrer pour sauvegarder vos changements dans le livre.

Une caractéristique intéressante est Points de contrôle. Avant de vous embarquer dans un ensemble ambitieux d’éditions, vous pouvez créer un point de contrôle. Le point de contrôle préservera l’état actuel de votre livre, dès lors si dans le futur vous décidez que vous n’appréciez pas les changements que vous lui avez fait vous pouvez revenir à l’état au moment où vous avez créer le point de contrôle. Pour créer un point de contrôle, utilisez Éditer → Créer un point de contrôle. Des points de contrôle seront aussi créés automatiquement pour vous au moment où vous exécutez n’importe quel outil automatisé comme le rechercher et remplacer global. La fonctionnalité point de contrôle est en complément au mécanisme normal Annuler/Restaurer lors de l’édition de fichiers individuels. Les points de contrôle vous sont utiles quand les changements sont disséminés sur de multiples fichiers dans le livre.

C’est le flux de travail basique pour l’édition de livres – Ouvrir un fichier, faire des changements, prévisualiser et sauver. Le reste de ce manuel traitera des divers outils et caractéristiques présents pour vous permettre d’exécuter efficacement des tâches spécifiques.

L’Explorateur de fichiers

Le Navigateur de fichiers vous donne un aperçu des différents fichiers à l’intérieur du livre que vous êtes occupé à éditer. Les fichiers sont arrangés par catégories, avec les fichiers texte (HTML) au sommet, suivis par les fichiers de feuilles de style (CSS), les images et ainsi de suite. Double cliquez simplement sur un fichier pour en commencer l’édition. L’édition est supportée pour le HTML, le CSS et les fichiers images. L’ordre des fichiers texte est le même ordre que celui dans lequel ils devraient être affichés, si vous êtes occupé de lire le livre. Tous les autres fichiers sont arrangés alphabétiquement.

En passant votre souris sur une entrée, vous pouvez voir sa taille, et aussi, au bas de l’écran, le chemin complet du fichier à l’intérieur du livre. Notez que les fichiers à l’intérieur des livres numériques sont compressés, aussi la taille du livre final n’est pas la somme des tailles des fichiers individuels.

Beaucoup de fichiers ont une signification spéciale, dans le livre. Ceux-ci auront généralement une icône à la suite de leurs noms, indiquant leur signification spéciale. Par exemple, dans l’image sur la gauche, vous pouvez voir que les fichiers cover_image.jpg et titlepage.xhtml ont l’icône d’une couverture à leur suite, ceci indique qu’ils sont l’image de couverture du livre et la page de titre. Similairement, le fichier content.opf à une icône métadonnée à sa suite, indiquant que les métadonnées du livre s’y trouvent et le fichier toc.ncx à une icône T à sa suite, indiquant que c’est la Table des Matières.

Vous pouvez effectuer beaucoup d’actions sur les fichiers individuels, en faisant un clic droit sur ceux-ci.

Renommer des fichiers

Vous pouvez renommer un fichier individuel en cliquant droit dessus et en sélectionnant Renommer. Renommer un fichier met automatiquement à jour les liens et les références à celui-ci tout au long du livre. Donc tout ce que vous avez à faire est de fournir un nouveau nom, calibre prendra soin du reste.

Vous pouvez également renommer beaucoup de fichiers en une fois. C’est utile si vous voulez que les fichiers aient un certain modèle de nom élémentaire. Par exemple, vous pourriez vouloir renommer tous les fichiers HTML pour avoir des noms Chapitre-1.html, Chapitre-2.html et ainsi de suite. Sélectionnez les fichiers que vous voulez renommer par lot en maintenant la touche Maj ou la touche Ctrl et en cliquant sur les fichiers. Puis cliquez droit et sélectionnez Renommer par lot. Entrez un préfixe et à quel nombre vous voudriez que la numérotation automatique commence, Cliquez OK et vous avez fini. La boîte de dialogue de renommage par lot vous permet également de renommer les fichiers selon l’ordre dans lequel ils apparaissent dans le livre au lieu de l’ordre dans lequel vous les avez sélectionnés, ce qui est utile, par exemple pour renommer toutes les images selon l’ordre dans lequel elles apparaissent.

Finalement, vous pouvez modifier l’extension de fichier par lot pour tous les fichiers sélectionnés. Sélectionnez plusieurs fichiers, comme ci-dessous, faites un clic droit et choisissez Modifier l’extension de fichier pour les fichiers sélectionnés.

Fusionner des fichiers

Parfois, vous pouvez vouloir fusionner deux fichiers HTML ou deux fichiers CSS ensemble. Il peut parfois être utile d’avoir tout dans un fichier unique. Soyez attentif, toutefois, que mettre beaucoup de contenu dans un fichier unique pose des problèmes de performance lorsque vous visualisez le livre dans un lecteur de livre numérique spécifique.

Pour fusionner de multiples fichiers ensemble, sélectionnez les en maintenant enfoncée la touche Ctrl (assurez vous de ne sélectionner des fichiers que d’un type, soit tous les fichiers HTML ou tous les fichers CSS et ainsi de suite). Faites alors un clic droit et sélectionner Fusionner. C’est tout. calibre fusionnera les fichiers, prendra automatiquement soin de faire la migration de tous les liens et références aux fichiers fusionnés. Notez que fusionner des fichiers peut parfois entraîner un changement du style de texte, puisque les fichiers individuels pourraient utiliser différentes feuilles de style.

Vous pouvez également sélectionner des fichiers texte, puis les glisser et les déposer sur un autre fichier texte pour fusionner les fichiers texte déposés dans le fichier texte cible.

Ordre de fichier texte changeant

Vous pouvez réorganiser l’ordre dans lequel les fichiers texte (HTML) sont ouverts lors de la lecture du livre en les faisant simplement glisser dans le Explorateur de fichiers ou en cliquant sur le fichier à déplacer puis en appuyant sur les modificateurs Ctrl+Shift avec les touches Haut, Bas, Début ou Fin. Pour les utilisateurs techniques, c’est ce qu’on appelle réordonner la structure organisationnelle du livre.

Notez que vous devez déposer les éléments entre d’autres éléments, et non sur eux, ce qui peut être un peu difficile jusqu’à ce que vous vous y habituiez. Si vous déposez un fichier au-dessus d’un autre, les fichiers seront fusionnés.

Marquage de la couverture

Les livres numériques ont normalement une image de couverture. Cette image est signifiée dans le Navigateur de fichiers par l’icône d’un livre brun suivant le nom de l’image. Si vous voulez désigner une autre image comme couverture, vous pouvez faire cela en faisant un clic droit sur le fichier et en choisissant Marquer comme image de couverture.

En complément, les fichiers EPUB ont le concept de « page de titre ». Une page de titre est un fichier HTML qui agit comme une page de titre/couverture pour le livre. Vous pouvez marquer un fichier HTML comme page de titre lors de l’édition des EPUBs en faisant un clic droit. Veillez que le fichier que vous marquez contient uniquement l’information de couverture. S’il contient une autre information, comme le premier chapitre, alors le contenu sera perdu si jamais l’utilisateur convertit l’EPUB dans calibre à un autre format. C’est parce que lors de la conversion, calibre suppose que la page de titre indiquée contient uniquement la couverture et pas d’autre contenu.

Suppression des fichiers

Vous pouvez supprimer des fichiers soit en faisant un clic droit sur ceux-ci, soit en appuyant sur la touche Supprimer. Supprimer un fichier retire toutes les références au fichier du fichier OPF, en enregistrant cette tâche. Cependant, les références dans les autres emplacements ne sont pas retirées, vous pouvez utiliser l’outil de Vérification de Livre pour facilement les trouver et les les supprimer/remplacer.

Exportation de fichiers

Vous pouvez exporter un fichier depuis l’intérieur du livre vers quelque part d’autre sur votre ordinateur. Ceci est utile si vous voulez travailler sur le fichier en isolation, avec des outils spécialisés. Pour faire ceci faire simplement un clic droit sur le fichier et choisissez Exporter.

Une fois que vous avez fini de travailler sur le fichier exporté, vous pouvez le ré-importer dans le livre, en faisant à nouveau un clic droit sur le fichier dans le livre et en choisissant Remplacer avec le fichier… qui vous permet de le fichier dans le livre avec le fichier précédemment exporté.

Vous pouvez également copier des fichiers entre plusieurs instances de l’éditeur. Sélectionnez les fichiers que vous voulez copier dans l”Explorateur de fichiers, puis cliquez droit et choisissez, Copier le fichier sélectionné vers une autre instance de l’éditeur. Ensuite, dans l’autre instance de l’éditeur, faites un clic droit dans l”Explorateur de fichiers et choisissez Coller le fichier à partir d’une autre instance de l’éditeur.

Ajout de nouvelles images/polices/etc. ou création de nouveaux fichiers vides

Vous pouvez ajouter une nouvelle image, police, feuille de style depuis votre ordinateur dans le livre en cliquant Fichier → Nouveau fichier. Ceci vous permet d’importer un fichier en cliquant sur le bouton Importer un fichier de ressources ou de créer un fichier HTML ou une feuille de style vierge en entrant simplement le nom du nouveau fichier.

Vous pouvez aussi importer de multiples fichiers dans le livre en une fois en utilisant Fichier->Importer des fichiers dans le livre.

Remplacement des fichiers

Vous pouvez facilement remplacer des fichiers existant dans le livre en faisant un clic droit sur le fichier et en choisissant remplacer. Ceci mettra automatiquement tous les liens et références à jour, dans le cas où le fichier de remplacement a un nom différent que le fichier en cours de remplacement.

Lier des feuilles de style aux fichiers HTML efficacement

Par commodité, vous pouvez sélectionner de multiples fichiers HTML dans le Navigateur de fichiers, faire un clic droit et choisir Lier les feuilles de style pour que calibre insère automatiquement les balises <link> pour ces feuilles de style dans tous les fichiers HTML sélectionnés.

Rechercher & remplacer

Éditer un livre possède une très puissante interface rechercher et remplacer qui vous permet de rechercher et remplacer du texte dans le fichier courant, à travers tous les fichiers et même dans une région marquée du fichier courant. Vous pouvez rechercher en utilisant une recherche normale ou en utilisant des expressions régulières. Pour apprendre comment utiliser des expressions régulières pour des recherches avancées, voir Tout à propos de l’utilisation des expressions régulières dans calibre.

Démarrer le Rechercher et Remplacer via l’entrée de menu Rechercher → Rechercher/Remplacer (vous devez être occupé à éditer un fichier HTML ou CSS).

Taper le texte que vous voulez trouver dans la case Chercher et sont remplacement dans la case Remplacer. Vous pouvez cliquer les boutons appropriés pour trouver la correspondance suivante, remplacer la correspondance actuelle et remplacer toutes les correspondances.

En utilisant les menus déroulants en bas de la boîte de dialogue, vous pouvez opérer la recherche sur le fichier courant, tous les fichiers textes, tous les fichiers de style ou tous les fichiers. Vous pouvez aussi choisir le mode de recherche à être une recherche normale (chaîne) ou une recherche par expression régulière.

Vous pouvez compter toutes les correspondances pour une expression de recherche via Rechercher → Tout compter. Le comptage s’effectuera sur n’importe quels fichiers/régions que vous avez sélectionnés dans les menus déroulants.

Vous pouvez aussi vous rendre à une ligne spécifique dans l’éditeur actuellement ouvert via Rechercher → Aller à la ligne.

Note

Rappelez-vous, pour vous armer de toute la puissance de la recherche et du remplacement, vous aurez besoin d’utiliser des expressions régulières. Voir : référence Tout à propos de l’utilisation des expressions régulières dans calibre.

Recherches sauvegardées

Vous pouvez sauvez les expressions rechercher/remplacer fréquemment utilisées ((inclus les expressions mode fonction) et les réutilisés de multiples fois. Pour sauver une recherche faites simplement un clic droit dans la case Recherche et sélectionner Sauvegarder la recherche actuelle.

Vous pouvez afficher les recherches sauvegardées via Rechercher → Recherches Sauvegardées. Ceci vous présentera une lise des expressions de recherche et de remplacement que vous pouvez appliquer. Vous pouvez même sélectionner des entrées multiples dans la liste en maintenant la touche Ctrl enfoncée pendant que vous cliquez pour exécuter de multiples expressions de recherche et remplacement en une seule opération.

Mode fonction

Le mode fonction vous permet d’écrire des fonctions Python arbitrairement puissantes qui sont exécutées sur chaque Rechercher/remplacer. Vous pouvez faire à peu près n’importe quelle manipulation de texte que vous souhaitez en mode fonction. Pour plus d’information, voir Mode fonction pour Rechercher & remplacer dans l’Éditeur.

Recherche ignorant les balises HTML

Il y a aussi un outil dédié pour la recherche de texte, ignorant toutes les balises HTML s’y trouvant. Par exemple, si le livre contient l’HTML Accent sur un <i>mot</i>. vous pouvez chercher sur un mot et il sera trouvé même s’il y a une balise <i> au milieu. Utilisez cet outil via l’élément du menu Rechercher → Rechercher en ignorant le balisage HTML

Outils automatisés

Editer le livre à divers outils pour aider avec les tâches communes. Ceux-ci sont accessibles via le menu Outils.

Edition de la Table des Matières

Il y a un outil dédié pour l’édition facile de la Table des Matières. Lancez le avec Outils → Table des Matières → Modifier Table des Matières.

L’outil éditer la Table des Matières vous affiche la Table des Matières actuelle (s’il y en a une) sur la gauche. Double cliquez simplement sur n’importe quelle entrée pour changer son texte. Vous pouvez aussi ré-organiser les entrées par glisser et déposer ou en utilisant les boutons sur la droite.

Pour les livres qui n’ont pas une Table des Matières préexistante. l’outil vous donne diverses options pour auto-générer une Table des Matières à partir du texte. Vous pouvez générer à partir des en-têtes dans le document, de liens, de fichiers individuels et ainsi de suite.

Vous pouvez éditer des entrées individuelles en cliquant dessus et en cliquant le bouton Changer l’emplacement vers lequel pointe cette entrée. Ceci ouvrira une mini prévisualisation du livre, déplacer simplement le curseur de souris sur le panneau de visualisation du livre, et cliquez où vous voulez que l’entrée pointe. Une ligne verte épaisse vous affichera l’emplacement. Cliquez OK une fois que vous êtes content avec l’emplacement.

Vérification du livre

L’outil Vérifier le livre recherche dans votre livre les problèmes qui pourraient l’empêcher de fonctionner comme prévu sur les périphériques de lecture actuels. Activez le via Outils → Vérifier le livre.

Tout problème trouvé est reporté dans une liste agréable, facile à utiliser. Cliquer n’importe quelle entrée dans la liste vous affiche une certaine aide à propos de cette erreur aussi bien que de vous donner l’option d’auto-réparer cette erreur, si cette erreur peut être réparée automatiquement. Vous pouvez aussi double cliquer sur l’erreur pour ouvrir l’emplacement de celle-ci dans un éditeur, vous pouvez ainsi la réparer vous même.

Certains des contrôles exécutés sont :

- Balise HTML mal formée. Toute balise HTML qui n’a pas été analysée comme XML bien formé est signalé. La corriger vous assurera que votre balise fonctionne comme attendu dans tous les contextes. calibre peut aussi auto réparé ces erreurs, mais l’auto réparation peut avoir parfois des effets inattendus, utilisez donc avec précaution. Comme toujours, un point de contrôle est créé avant l’auto réparation vous pouvez donc annuler tous les changements. L’auto réparation fonctionne en analysant la balise utilisant l’algorithme HTML5, qui est fortement sensible aux défaillances et convertit alors en XML bien formé.

- Styles CSS mal formés ou inconnus. Tout CSS qui n’est pas valide ou donrt les propriétés ne sont pas définies dans le standard CSS 2.1 (plus un peu de CSS 2.3) sont signalées. Le CSS est vérifié dans toutes les feuilles de style, attributs de style intégrés et les balises <style> dans les fichiers HTML.

- Les liens cassés. Les liens qui pointent vers des fichiers à l’intérieur du livre qui sont manquant sont signalés.

- Fichiers non référencés. Fichiers dans le livre qui ne sont pas référencés par tout autre fichier ou ne sont pas dans la structure organisationnelle sont signalés.

- Divers problèmes habituels dans les fichiers OPF tels que la structure organisationnelle ou des éléments du manifeste en double, des idrefs ou des étiquettes meta couverture cassées, des sections requises manquantes et ainsi de suite.

- Diverses vérifications de compatibilité pour les problèmes connus qui entraînent un mauvais fonctionnement du livre sur des périphériques de lecture.

Ajout d’une couverture

Vous pouvez facilement ajouter une couverture au livre via Outils → Ajouter une couverture. Ceci vous permet soit de choisir une image existante dans le livre comme couverture ou d’importer une nouvelle image dans le livre et en faire la couverture. Lors de l’édition de fichiers EPUB, l’examen de l’HTML pour la couverture est automatiquement généré. Si une couverture existante pour le livre est trouvée, elle est remplacée. L’outil prend automatiquement soin de marquer les fichiers couverture comme couvertures dans l’OPF.

Intégration des polices référencées

Accédé via Outils → Inclure les polices référencées, cet outil trouve toutes les polices référencées dans le livre et si elles ne sont pas encore inclusent, les recherche sur votre ordinateur et les inclus dans le livre, s’il les trouve. Assurez-vous s’il vous plaît que vous avez les droits de copie nécessaires pour inclure les polices à licence commerciale, avant de faire ceci.

Créer un sous-ensemble de polices

Accédé via Outils → Sous-ensemble de polices incluses, cet outil réduit toutes les polices dans le livre pour contenir uniquement des glyphes pour le texte actuellement présent dans le livre. Ceci réduit habituellement la taille des fichiers de police de ~ 50%. Cependant, soyez conscient qu’une fois les polices sont en sous-ensemble, si vous ajouter du nouveau texte dont les caractères ne sont pas précédemment présents dans le sous-ensemble de police, la police ne fonctionnera pour le nouveau texte. Aussi faites ceci uniquement comme dernière étape de votre plan de travail.

Amélioration de la ponctuation

Converti les tirets, points de suspension, guillemets, multples traits d’union, etc. en texte clair en leurs équivalents typographiquement corrects. Notez que l’algorithme peut parfois générer des résultats incorrects, particulièrement quand des apostrophes au début des contractions sont impliquées. Accédé via ;guillabel:Outils->Ponctuation intelligente.

Transformation des propriétés CSS

Créer des règles pour transformer l’esthétique du livre. Par exemple, créer une règle pour convertir tout le texte rouge en vert ou doubler la taille de police de tout le texte dans le livre ou faire le texte d’une certaine famille de police italique, etc.

La création de règles est simple, les règles suivent un format de langage naturel, cela ressemble à :

- Si la propriété color est rouge changez la en vert

- Si la propriété font-size est n’importe quelle valeur multiplier la valeur par 2

Accessible via Outils → Transformer les styles.

Suppression en cours des règles CSS inutilisées

Supprimer toutes les règles CSS inutilisées des feuilles de style et des balises <style>. Certains livres créés à partir de modèles de production peuvent avoir un grand nombre de règles CSS supplémentaires qui ne correspondent à aucun contenu actuel. Ces règles supplémentaires peuvent ralentir les lecteurs qui ont besoin de toutes les tracer. Accédez via Outils → Supprimer les règles CSS inutilisées.

Correction de l’HTML

Cet outil convertit simplement de l’HTML qui ne peut pas être analysé comme du XML en XML bien formé. Il est très courant dans les livres numériques d’avoir du XML mal formé, cette outil automatise donc simplement le processus de réparation d’un tel HTML. L’outil fonctionne en analysant l’HTML en utilisant l’algorithme de l’HTML5 (l’algorithme utilisé dans tous les navigateurs récents) et en convertissant alors le résultat en XML. Soyez conscient que l’auto réparation peut parfois avoir des résultats inattendus. Si vous préférez, vous pouvez utiliser l’outil de vérification de livre numérique présenté plus haut pour trouver et corriger manuellement les problèmes dans l’HTML. On y accède via Outils → Réparer l’HTML.

Embellissement des fichiers

Cet outil est utilisé pour auto formater les fichiers HTML et CSS afin qu’ils aient une « belle apparence ». Le code est auto indenté afin qu’il s’aligne joliment, les interlignes sont insérés là où c’est approprié et ainsi de suite. Notez qu’embellir auto répare également l’HTML/CSS abîmé. Dès lors, si vous ne voulez qu’aucune auto réparation ne soit effectuée, utiliser d’abord l’outil Vérifier le livre pour corriger tous les problèmes et seulement alors exécuter embellir. Accédez via Outils → Embellir tous les fichiers.

Note

En HTML tout texte peut avoir un espacement significatif, par l’intermédiaire de la directive CSS white-space. Par conséquent, l’embellissement pourrait potentiellement changer le rendu de l’HTML. Pour éviter cela autant que possible, l’algorithme d’embellissement embellit uniquement des balises de bloc de niveau qui contiennent d’autres balises de bloc de niveau.. Ainsi, par exemple, du texte à l’intérieur d’une balise <p> n’aura pas ses espacements changés. Mais une balise <body> qui contient seulement d’autres balises <p> et <div> sera embellie. Ceci peut parfois indiquer qu’un fichier particulier ne sera pas affecté par l’embellissement car il n’a pas les balises de niveau de bloc appropriées. Dans de tels cas vous pouvez essayer différents outils d’embellissement, qui font moins attention, par exemple : HTML Tidy [https://infohound.net/tidy/].

Insertion d’une Table des Matières

Normalement dans les livres numériques, la Table des Matières est séparée du texte principal et est typiquement accédée par l’intermédiaire d’un bouton/menu spécial dans le périphérique de lecture de livre numérique. Vous pouvez également voir calibre générer automatiquement une Table des Matières « en interne » qui devient une partie du texte du livre. Elle est générée sur base de la Table des Matières actuellement définie.

Si vous utilisez cet outil de multiples fois, chaque invocation entraînera le remplacement de la Table des Matières précédemment créée. L’outil peut être accédé via Outils → Table des Matières → Insérer une Table des Matières.

Configuration de Sémantique

Cet outil est utilisé pour paramétrer les « sémantiques » dans les fichiers EPUB. Les sémantiques sont simplement, des liens dans le fichier OPF qui identifient certains emplacements dans le livre comme ayant une signification spéciale. Vous pouvez les utiliser pour identifier l’avant-propos, la dédicace, la couverture, la table des matières, etc. Choisissez simplement le type d’information sémantique que vous voulez spécifier et choisissez dans le livre l’emplacement vers lequel le lien devra pointer. Cet outil peut être accédé via Outils → Paramétrer sémantique.

Filtrage des informations de style

Cet outil peut être utilisé pour facilement supprimer des propriétés de style CSS spécifiées de l’entièreté du livre. Vous pouvez lui indiquer quelles propriétés vous voulez supprimer, par exemple, color, background-color, line-height et il les supprimera partout où elles surviennent — feuilles de style, étiquettes <style> et attributs internes « style ». Après la suppression les informations de style, un sommaire des tous les changements effectués est affiché ainsi vous pouvez voir exactement ce qui a été changé. L’outil peut être accédé via Outils → Filtrer l’information de style.

Mise à jour des éléments du livre

Cet outil peut être utilisé pour mettre à jour les éléments internes du livre, si possible. Par exemple, il mettra à jour les livres EPUB 2 en livres EPUB 3. L’outil est accessible via Mise à jour des éléments du livre.

Points de Contrôle

Points de contrôle est un moyen d’indiquer l’état actuel du livre comme étant « spécial ». Vous pouvez alors y aller et faire tous les changements que vous voulez au livre et si vous n’appréciez pas les résultats, revenir au point de contrôle. Les points de contrôle sont créés automatiquement chaque fois que vous exécutez n’importe lequel des outils automatisés décrit dans la section précédente.

Vous pouvez créer un point de contrôle via guilabel:Editer->Créer un point de contrôle. Et revenir à un point de contrôle précédent avec Editer → Rétablir …

La fonctionnalité point de contrôle est en addition au mécanisme normal Annuler/Refaire lors de l’édition de fichiers individuels. Les points de contrôles sont nécessaires lorsque les changements sont éparpillés sur de multiples fichiers dans le livre ou quand vous souhaitez être en possibilité d’annuler dans leur ensemble un grand nombre de changements qui s’y rapporte.

Vous pouvez voir une liste des points de contrôle disponibles via Aperçu → Points de contrôle. Vous pouvez comparer l’état actuel du livre à un point de contrôle particulier en utilisant l’outil Comparer des livres numériques – en sélectionnant le point de contrôle intéressé et en cliquant le bouton Comparer. Le bouton Revert to restaure le livre au point de contrôle sélectionné, annulant tous les changements depuis que ce point de contrôle a été créé.

Le Panneau de prévisualisation directe

La Prévisualisation de fichier vous donne un aperçu des différents fichiers intégrés. Le panneau de prévisualisation en direct vous montre les changements que vous êtes occupé à faire (avec deux ou trois secondes de délai). Au fur et à mesure que vous éditez les fichiers HTML et CSS, le panneau de prévisualisation est automatiquement mis à jour pour refléter vos changements. En même temps que vous bougez le curseur sur l’éditeur, le panneau de prévisualisation traquera son emplacement, vous montrant l’emplacement correspondant dans le livre. En cliquant dans le panneau de prévisualisation, cela entraînera le positionnement du curseur dans l’éditeur sur l’élément que vous avez cliqué. Si vous cliquez un lien pointant vers un autre fichier dans le livre, ce fichier sera ouvert dans l’éditeur et le panneau de prévisualisation, automatiquement.

Vous pouvez désactiver la synchronisation automatique entre l’emplacement et la prévisualisation en direct des changements – par les boutons se situant sous le panneau de prévisualisation. La mise à jour en direct du panneau de configuration a lieu uniquement quand vous n’êtes pas activement occupé à taper dans l’éditeur, ceci afin de ne pas vous distraire ou vous ralentir, attendre pour que la prévisualisation soit fournie.

Le panneau de prévisualisation vous montre à quoi ressemblera le texte quand il est visionné. Cependant, le panneau de prévisualisation n’est pas un substitut pour tester votre livre sur un périphérique de lecture. Il est à la fois plus et moins capable qu’un lecteur. Il tolérera des erreurs et des balises négligées bien mieux que beaucoup de périphériques de lecture. Il ne vous affichera pas non plus les marges de page, les sauts de page et les polices intégrées qui utilisent des alias de nom de police. Utiliser le panneau de prévisualisation pendant que vous travaillez sur le livre, mais une fois que vous avez fini, révisez-le dans un périphérique de lecture ou dans un émulateur.

Note

Le panneau de prévisualisation ne supporte pas les fontes intégrées si le nom de la police dans le fichier de police ne correspond pas au nom dans la règle CSS @font-face. Vous pouvez utiliser l’outil Vérifier le livre pour trouver rapidement et réparer tous types de problèmes de police.

Fractionnement des fichiers HTML

Une utilisation, peut-être non évidente, du panneau de prévisualisation est de fractionner de longs fichiers HTML. Pendant la visualisation du fichier que vous voulez fractionner, cliquez le bouton Mode fractionné en dessous du panneau de prévisualisation . Bouger alors simplement votre souris jusqu’à l’endroit où vous voulez fractionner le livre et cliquez. Une épaisse ligne verte vous montrera ou exactement le fractionnement s’effectuera au fur et à mesure que vous bougez votre souris. Une fois que vous avez trouvé l’emplacement que vous voulez, cliquez simplement et le fractionnement sera effectué.

Fractionner le fichier mettra automatiquement à jour tous les liens et les références qui pointent dans la moitié inférieure du fichier et ouvrira le nouveau fichier fractionné dans un éditeur.

Vous pouvez aussi fractionner un fichier HTML simple en de multiples emplacements automatiquement, en faisant un clic droit à l’intérieur du fichier dans l’éditeur et en choisissant Fractionner en de multiples emplacements. Ceci vous permettra de fractionner facilement un grand fichier à toutes les balises de titre ou à toutes les balises ayant une certaine classe et ainsi de suite.

Le panneau CCS en temps réel

Le panneau CSS en temps réel vous affiche toutes les règles de style qui s’appliquent à la balise que vous êtes occupé à éditer. Le nom de la balise, avec son numéro de ligne dans l’éditeur est montré, suivi par une liste des règles de style correspondantes.

C’est une excellente manière de voir rapidement quelles règles de style s’appliquent à n’importe quelle balise. La vue à aussi des liens cliquables (en bleu), qui vous amène directement à l’emplacement où le style a été défini, dans le cas où vous souhaitez faire n’importe quel changement à la règle de style. Les règles de style qui s’appliquent directement à la balise, aussi bien que les règles qui sont héritées de balises parents sont montrées.

Le panneau vous montre aussi quels sont les styles finalement calculés pour la balise. Les propriétés dans la liste qui sont supplantées par des règles de plus haute priorité sont montrées avec une ligne au travers de celles-ci.

Vous pouvez activer le panneau CSS en temps réel via Aperçu → CSS en temps réel.

Outils divers

Il y a quelques outils de plus qui peuvent être utiles quand vous éditez le livre.

La vue Table des Matières

La vue Table des Matières vous montre la table des matières actuelle dans le livre. Faire un double clic sur n’importe quelle entrée ouvre l’endroit où cette entrée pointe dans un éditeur. Vous pouvez faire un clic droit pour éditer la Table des Matières, rafraîchir la vue ou développer/réduire tous les éléments. Accédez à cette vue via Aperçu → Table des Matières.

Vérification de l’orthographe des mots dans le livre

Vous pouvez exécuter une vérificateur orthographique via Outils → Vérifier l’orthographe.

Les mots sont affichés avec le nombre de fois où ils apparaissent dans le livre et la langue à laquelle le mot appartient. L’information de langue est récupérée des métadonnées des livres et des attributs lang dans les fichiers HTML. Ceci permet au vérificateur d’orthographe de bien fonctionner même avec des livres qui contiennent du texte dans différentes langues. Par exemple, dans l’extrait HTML qui suit, le mot color sera vérifié en utilisant American English et le mot colour en utilisant British English:

<div lang="en_US">color <span lang="en_GB">colour</span></div>

Note

Vous pouvez faire double cliquer un mot pour mettre en évidence l’occurrence suivante de ce mot dans l’éditeur. Ceci est utile si vous souhaitez éditer manuellement le mot, ou voir dans quel contexte il se situe.

Pour changer un mot, double cliquez simplement l’une des alternatives suggérée sur la droite, ou entrer votre propre orthographe corrigée et cliquez le bouton Changer le mot sélectionné par. Ceci remplacera toutes les occurrences du mot dans le livre. Vous pouvez aussi faire un clic droit sur un mot dans la liste de mots principale pour changer le mot aisément à partir du menu contextuel.

Vous pouvez faire en sorte que le vérificateur d’orthographe ignore un moment pour la session courante en cliquant le bouton Ignorer. Vous pouvez aussi ajouter un mot au dictionnaire utilisateur en cliquant le bouton Ajouter au dictionnaire. Le vérificateur d’orthographe supporte de multiples dictionnaires utilisateur, vous pouvez donc sélectionner le dictionnaire auquel vous voulez ajouter le mot.

Vous pouvez aussi faire afficher par le vérificateur d’orthographe tous les mots dans votre livre, pas uniquement ceux qui sont mal orthographiés. Ceci est utile pour voir quels mots sont les plus communs dans votre livre et effectuer un simple rechercher et remplacer sur des mots individuels.

Note

Si vous effectuez n’importe quel changement au livre en l’éditant pendant la vérification orthographique, vous devrez cliquer le bouton Actualiser dans l’outil de vérification d’orthographe. Si vous ne faites pas cela et continuez d’utiliser l’outil de Vérification d’orthographe, vous pourriez perdre les changements que vous avez effectués dans l’éditeur.

Note

Pour exclure un fichier individuel de la vérification orthographique lors de l’exécution de l’outil de vérification orthographique, vous pouvez utiliser le bouton Exclure les fichiers ou ajouter le commentaire suivant juste sous la balise d’ouverture du fichier

<!-- calibre-no-spell-check -->

Ajouter de nouveaux dictionnaires

Le vérificateur d’orthographe est fourni avec des dictionnaires intégrés pour les langues Anglaises et Espagnoles. Vous pouvez installer vos propres dictionnaires via Editer → Préférences → Propriètés de l’éditeur → Organiser les dictionnaires orthographiques. Le vérificateur orthographique utiliser les dictionnnaires diu programme LibreOffice (au format .oxt). Vous pouvez télécharger ces dictionnaires depuis le dépôt d’Extensions LibreOffice <https://extensions.libreoffice.org/?Tags%5B%5D=50>`_.

Insérer des caractères spéciaux

Vous pouvez insérer des caractères qui sont difficiles à taper en utilisant l’outil Editer → Insérer un caractère spécial. Ceci vous montre tous les caractères Unicode, cliquez simplement le caractère que vous voulez taper. Si vous maintenez la touche Ctrl pendant que vous cliquez, la fenêtre se fermera d’elle-même après l’insertion du caractère sélectionné. Cet outil peut être utiliser pour insérer des caractères spéciaux dans le texte principal ou dans n’importe quel espace de l’interface utilisateur, comme l’outil Rechercher et remplacer.

Parce qu’il y a beaucoup de caractères, vous pouvez définir vos propres caractères Préférés, qui seront montrés en premier. Faites simplement un clic droit sur un caractère pour le marquer comme favori. Vous pouvez aussi faire un clic droit sur un caractère dans préférés pour l’en retirer. Finalement, vous pouvez réarranger l’ordre des caractères dans préférés en cliquant le bouton Réarranger les préférés et alors glisser et déposer les caractères dans les favoris.

Vous pouvez aussi saisir directement des caractères spéciaux en utilisant le clavier. Pour faire cela, vous saisissez le code Unicode pour le caractère (en hexadécimal) et appuyez alors les touches Alt+X ce qui convertira le code précédemment saisi dans le caractère correspondant. Par exemple, pour saisir ÿ vous devrez saisir ff et ensuite :kbd:Alt+X. Pour saisir un espace insécable vous devrez utiliser a0 et ensuite Alt+X, pour saisir des points de suspension vous devrez utiliser 2026 et ensuite Alt+X et ainsi de suite.

Finalement, vous pouvez saisir des caractères spéciaux en utilisant les entités nommées HTML. Par exemple, saisir &nbsp; sera remplacé par un espace insécable lorsque vous saisissez le point-virgule. Le remplacement survient uniquement quand vous saisissez le point-virgule.

La vue inspecteur de code

Cette vue vous montre le codage HTML et CSS qui s’applique à l’élément actuel d’intérêt. Vous l’ouvrez par un clic droit sur un emplacement dans le panneau de prévisualisation et en choisissant Inspecter. Il vous permet de voir le codage HTML pour cet élément et d’une manière plus primordiale, les styles CSS qui s’y appliquent. Vous pouvez même dynamiquement éditer les styles et voir immédiatement quels effets donnent vos changements. Notez que l’édition des styles n’apporte pas réellement des modifications au contenu de livre, il permet seulement de l’expérimentation rapide. La capacité d’éditer en direct à l’intérieur de l’Inspecteur est en cours de développement.

Vérifications des liens externes

Vous pouvez utiliser cet outil pour vérifier tous les liens dans votre livre qui pointent vers des sites web externes. L’outil essayera de visiter chaque site web lié à l’extérieur, et si la visite échoue, il vous rapportera tous les liens cassés dans un format pratique pour les réparer.

Téléchargement de ressources externes

Vous pouvez utiliser cet outil pour télécharger automatiquement n’importe quel images/feuilles de style/etc. dans le livre qui ne sont pas fournies avec le livre (Par ex : il a des URL pointant vers un endroit sur Internet). L’outil retrouvera tout ces types de ressources et les téléchargera automatiquement, les ajoutera au livre et remplacera toutes les références à celles-ci pour utiliser les fichiers téléchargés.

Organisation des fichiers par type dans des dossiers

Souvent en éditant les fichiers EPUB que vous obtenez de quelque part, vous constaterez que les fichiers à l’intérieur de l’EPUB sont arrangés au petit bonheur la chance, dans différents sous-dossiers. Cet outil vous permet d’entrer automatiquement tous les fichiers dans des sous-dossiers basés sur leurs types. Accédez-y par l’intermédiaire de Outils → Organiser dans des dossiers. Notez que cet outil change seulement la manière dont les fichiers sont arrangés à l’intérieur de l’EPUB, il ne change pas la manière dont ils sont affichés dans le Navigateur de fichiers.

Importation de fichiers dans d’autres formats de livres numériques comme l’EPUB

L’éditeur inclut la capacité d’importer des fichiers de certains autres formats de livre numérique directement comme un nouvel EPUB, sans passer par une pleine conversion. C’est particulièrement utile pour créer directement des fichiers EPUB à partir de vos propres fichiers HTML édités à la main. Vous pouvez faire ceci par l’intermédiaire de Fichier → Importer un fichier HTML ou DOCX en tant que nouveau livre.

L’outil Rapports

L’éditeur inclut un outil sympa Rapports (via ;guilabel:Outils->Rapports) qui affiche des résumés des fichiers, images, mots, caractères et styles utilisés dans le livre. Chaque ligne dans le rapport est liée. Double cliquer une ligne envoie à l’endroit dans le livre où cet élément est utilisé ou défini (selon le cas). Par exemple, dans la vue Liens, vous pouvez double cliquer les entrées de la colonne Source pour sauter là où le lien est défini et les entrées dans la colonne Cible pour sauter là où le lien pointe.

Fonctionnalités spéciales dans l’éditeur de code

L’éditeur HTML calibre est très puissant. Il a beaucoup de fonctionnalités qui rendent l’édition de l’HTML (et du CSS) plus facile.

Mise en évidence de la syntaxe

L’éditeur HTML a des mises en évidence très sophistiquées de la syntaxe. Les fonctionnalités comprennent :

- Le texte inclut dans des balises gras, italique et titre est établi en gras/italique

- Au fur et à mesure que vous bougez votre curseur au travers de l’HTML, les balises HTML correspondantes sont mises en surbrillance, et vous pouvez sauter vers la balise ouvrante ou fermante avec les raccourcis clavier Ctrl+{ et :kdb:`Ctrl+}`. De même, vous pouvez sélectionner le contenu d’une balise avec Ctrl+Alt+T ou Ctrl+Shift+T

- L’HTML invalide est mis en surbrillance avec un soulignement rouge

- Les erreurs d’orthographe dans le texte inclut dans les balises HTML et les attributs comme titre sont mis en surbrillance. La correction orthographique est fonction de la langue, basée sur la valeur de l’attribut lang de la balise courante et la langue globale du livre.

- Le CCS intégré dans des balises <style> est mis en surbrillance

- Les caractères spéciaux qui peuvent être difficiles à distinguer comme les espaces insécables, différents types de traits d’union, etc. sont mis en surbrillance.

- Les liens vers d’autres fichiers dans des balises <a>, des balises <img> et <link> ont tous leurs noms de fichiers mis en surbrillance. Si le nom de fichier vers lequel il pointe n’existe pas, le nom de fichier est marqué avec un soulignement rouge.

Aide sensible au contexte

Vous pouvez faire un clic droit sur une balise HTML ou une propriété CSS pour obtenir de l’aide sur cette balise ou propriété.

Vous pouvez également maintenir enfoncée la touche Ctrl et cliquer sur n’importe quel nom de fichier inclut dans une balise link pour ouvrir automatiquement ce fichier dans l’éditeur. De même, Ctrl en cliquant sur le nom d’une classe vous amènera à la première règle de style qui correspond à la balise et à la classe.

Un clic droit sur le nom d’une classe dans un fichier HTML vous permettra de renommer la classe, ce qui modifiera toutes les occurrences de la classe dans le livre et toutes ses feuilles de style.

Auto-complétion

Pendant l’édition d’un livre numérique, l’une des tâches des plus fastidieuses est la création de liens vers d’autres fichiers dans le livre, ou vers les feuilles de style CSS ou les images. Vous devez indiquer le nom du fichier correct et le chemin relatif au fichier. L’éditeur a une auto-complétion pour faciliter cela.

Au fur et à mesure que vous tapez un nom de fichier, l’éditeur fait apparaître automatiquement des suggestions. Utilisez simplement la touche Tabulation pour sélectionner le nom de fichier correct. L’éditeur offre même des suggestions pour des liens pointant vers une ancre dans un autre fichier HTML. Après que vous ayez tapé le caractère #, l’éditeur vous montrera une liste de toute les ancres dans le fichier cible, avec un petit extrait de texte pour vous aider à choisir la bonne ancre.

Notez que contrairement à la plupart des autres systèmes de complétion, la système de complétion de l’éditeur utilise la correspondance en sous-séquence. Cela signifie que vous pouvez taper seulement deux ou trois lettres de n’importe où dans le nom de ficher pour compléter le nom de fichier. Par exemple, disons que vous voulez le nom de fichier ../images/arrow1.png, vous pouvez simplement taper ia1 et appuyer sur la touche Tabulation pour compléter le nom de fichier. Lors de la recherche de correspondances, le système de complétion priorise les lettres qui sont au début d’un mot, ou immédiatement après un séparateur de chemin. Une fois que aurez pris l’habitude d’utiliser ce système, vous trouverez qu’il vous fait gagner beaucoup de temps et d’efforts.

Fragments

L’éditeur de livre numérique de calibre supporte les fragments. Un fragment est un morceau de texte qu est soit souvent réutilisé soit contient beaucoup de texte redondant. L’éditeur vous permet d’insérer un fragment avec seulement quelques séquences de touches. Les fragments sont très puissants, avec beaucoup de fonctionnalités, comme les emplacements d’où vous pouvez sauter de l’un à l’autre, refléter automatiquement le texte répété et ainsi de suite. Pour plus d’informations, voir fragments.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Éditer des livres numériques »

- Mode fonction pour Rechercher & remplacer dans l’Éditeur

Mode fonction pour Rechercher & remplacer dans l’Éditeur

L’outil Rechercher & remplacer dans l’éditeur supporte un mode fonction. Dans ce mode, vous pouvez combiner des expressions régulières (voir Tout à propos de l’utilisation des expressions régulières dans calibre) avec de puissantes fonctions Python pour faire toutes sortes de traitements avancés de texte.

Dans le mode standard regexp pour rechercher et remplacer, vous spécifiez tant une expression régulière à rechercher qu’un modèle qui est utilisé pour remplacer toutes les correspondances trouvées. Dans le mode fonction, à la place d’utiliser un modèle fixe, vous spécifiez une fonction quelconque, dans le langage de programmation Python [https://docs.python.org]. Ceci vous permet de faire beaucoup de choses qui ne sont pas possibles avec de simples modèles.

Les techniques pour utiliser le mode fonction et la syntaxe seront décrites à l’aide d’exemples, vous montrant comment créer des fonctions pour effectuer progressivement des tâches plus complexes.

Réparer automatiquement la casse des titres dans le document

Ici , nous utiliserons l’une des fonctions intégrée dans l’éditeur pour changer automatiquement la casse de tous le texte à l’intérieur des balises titre en casse titre

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

Pour la fonction, choisissez simplement la fonction intégrée Texte en casse de titre(ignorer les balises). Celle-ci changera les titres qui ressemblent à : <h1>certains TITRES</h1> en <h1>Certains Titres</h1>. Elle fonctionnera même s’il y a d’autres balises HTML dans les balises de titre.

Votre première fonction personnalisée - les traits d’union d’embellissement

La vraie puissance du mode fonction vient du fait de pouvoir créer vos propres fonctions pour traiter le texte de manières quelconques. L’outil de Ponctuation Intelligente dans l’éditeur laisse les traits d’union individuels de côté, vous pouvez employer cette fonction pour les remplacer par des tirets fins.

Pour créer une nouvelle fonction, cliquez simplement le bouton Créer/Éditer pour créer une nouvelle fonction et copiez le code Python qui se trouve ci-dessous.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

Chaque fonction personnalisée Rechercher & remplacer doit avoir un nom unique et consister en une fonction Python nommée replace, qui accepte tous les arguments affichés ci-dessus. Pour le moment, nous ne nous inquiéterons pas à propos de tous les différents arguments de la fonction replace(). Focalisons-nous uniquement sur l’argument match. Il représente une correspondance lors de l’exécution d’un rechercher et remplacer. Sa documentation complète est disponible ici [https://docs.python.org/library/re.html#match-objects]. match.group() renvoie simplement tout le texte correspondant et tout ce que nous faisons est de remplacer les traits d’union dans ce texte par des tirets fins, en remplaçant d’abord les doubles traits d’union et ensuite les traits d’union seuls.

Utilisez cette fonction avec l’expression régulière de recherche

>[^<>]+<

Et il remplacera tous les traits d’union par des tirets fins, mais seulement dans le texte actuel et non dans les définitions de balises HTML.

La force du mode fonction - utiliser un dictionnaire orthographique pour réparer les mots aux mauvais traits d’union

Souvent les livres numériques créés à partir de scans de livres imprimés contiennent des mots avec de mauvais traits d’union – les mots qui sont divisés à la fin de la ligne de la page imprimée. Nous écrirons une fonction simple pour trouver et réparer automatiquement de tels mots.

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

Utilisez cette fonction avec la même expression de recherche que précédemment, à savoir

>[^<>]+<

Et il réparera magiquement tous les mots avec de mauvais traits d’union dans le texte du livre. L’astuce principale est d’utiliser l’un des utiles arguments supplémentaires de la fonction de remplacement, dictionaries. Celui-ci se réfère aux dictionnaires que l’éditeur utilise lui-même pour vérifier orthographiquement le texte dans le livre. Ce que fait cette fonction est de regarder aux mots séparés par un trait d’union, supprimer le trait d’union et vérifier si le dictionnaire reconnait le mot composé, s’il le fait, les mots originaux sont remplacés par le mot composé libre du trait d’union.

Notez qu’une limitation à cette technique est qu’elle ne fonctionnera uniquement qu’avec les livres unilingues, car, par défaut, dictionaries.recognized() utilise la langue principale du livre.

Auto numérotation des sections

Maintenant nous allons voir quelque chose d’un peu différent. Supposons que votre fichier HTML ait beaucoup de sections, chacune avec un titre dans une balise <h2> ressemblant à <h2>Un certain texte</h2>. Vous pouvez créer une fonction personnalisée qui numérotera automatiquement ces titres avec des numéros de section consécutifs, afin qu’elles ressemblent à <h2>1. Un certain texte</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'

Utilisez la avec l’expression de recherche

(?s)(<h2[^<>]*>)(.+?</h2>)

Placez le curseur en haut du fichier et cliquez Remplacez tout

Cette fonction utilise l’un des utiles arguments supplémentaires pour replace() l’argument ``number`. Lorsque vous faites un Remplacer Tout le nombre est incrémenté automatiquement pour chaque correspondante successive .

Une autre nouvelle fonctionnalité est l’utilisation de replace.file_order` – régler cela à spine signifie que si cette recherche est exécutée sur de multiples fichiers HTML, ces fichiers seront traités dans l’ordre dans lequel ils apparaissent dans le livre. Voir Choisissez l’ordre de fichier lors de l’exécution sur de multiples fichiers HTML pour des détails.

Auto créer une Table des Matières

Finalement, essayons quelque chose d’un peu plus ambitieux. Supposons que votre livre à des titres dans des balises h1 et h2 qui ressemblent à <h1 id= »someid »>Un certain texte</h1>``. Nous auto générerons une Table des Matières HTML basée sur ces titres. Créez la fonction personnalisée suivante :

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'

Et utilisez là pour trouver l’expression:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

Lancez la recherche sur Tous les fichiers textes et à la fin de la recherche, une fenêtre apparaîtra avec « Résultat de débogage pour votre fonction » qui contiendra la Table des Matières HTML, prête à être collée dans toc.html.

La fonction ci-dessus est fortement commentée, aussi elle devrait être facile à suivre. La nouvelle fonctionnalité clé est l’utilisation d’un autre argument supplémentaire utile à la fonction replace(), l’objet data. L’objet data est un dictionnaire Python qui persiste entre les invocations successives de replace() pendant une seule opération Remplacer Tout.

Une autre nouvelle fonctionnalité est l’utilisation de call_after_last_match – paramétrer cela à True sur la fonction replace() signifie que l’éditeur appellera replace() une fois de plus après que toutes les correspondances aient été trouvées. Pour cet appel supplémentaire, l’objet correspondant sera``None``

Ceci était juste une démonstration pour vous montrer la puissance du mode fonction. Si vous avez réellement besoin de générer une Table des Matières à partir des titres dans votre livre, vous aurez mieux en utilisant l’outil Table des Matières dédié dans Tools → Table des Matières.

L’API pour le mode fonction

Toutes les fonctions du mode fonction doivent être des fonctions Python nommées replace, avec la signature suivante

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return a_string

Quand un rechercher/remplacer est exécuté, pour chaque correspondance qui est trouvée, la fonction replace() sera appelée, elle doit retourner la chaîne de remplacement pour cette correspondance. Si aucuns remplacements ne doivent être effectués, elle devrait retourner match.group() qui est la chaîne originale. Les divers arguments de la fonction replace() sont documentées ci-dessous.

L’argument match

L’argument match représente la correspondance actuellement trouvée. C’est un objet Python Match [https://docs.python.org/library/re.html#match-objects]. Sa méthode la plus utile est group() qui peut être utilisée pour récupérer le texte apparié correspondant à la capture de groupes individuels dans l’expression régulière de recherche.

L’argument number

L’argument number est le nombre de l’actuelle correspondance. Lorsque vous exécutez Remplacer Tout, chaque correspondance successive entraînera replace() à être appelé avec un nombre incrémenté. La première correspondance porte le numéro 1.

L’argument file_name

Ceci est le nom du fichier dans lequel la correspondance actuelle a été trouvée. Lors d’une recherche à l’intérieur d’un texte marqué, le file_name est vide. Le file_name est de forme reconnue, une chemin relatif à la racine du livre, utilisant / comme séparateur de chemin.

L’argument metadata

Ceci représente les métadonnées du livre actuel, comme le titre, les auteurs, la langue, etc. C’est un objet de la classe calibre.ebooks.metadata.book.base.Metadata. Les attributs utiles incluent, title, authors (une liste d’auteur) et language (le code de langue).

L’argument dictionaries

Ceci représente la collection de dictionnaires utilisés pour la vérification orthographique du livre actuel. Sa méthode la plus utile est dictionaries.recognized(word) qui renverra ``True```si le mot analysé est reconnu par le dictionnaire de la langue actuelle du livre.

L’argument data

Ceci est un un simple dictionnaire Python. Lorsque vous exécutez Remplacer tout, toutes les correspondances suivantes entraîneront un appel de replace() avec le même dictionnaire en tant que données. Vous pouvez donc l’utiliser pour stocker arbitrairement des données entre des invocations de replace() pendant une opération Remplacer tout.

L’argument functions

L’argument functions vous donne accès à toutes les autres fonctions définies par l’utilisateur. Ceci est utile pour ré-utiliser du code. Vous pouvez définir les fonctions utilitaires à un seul endroit et les ré-utiliser dans toutes vos autres fonctions. Par exemple, supposons la création d’une fonction nommée My Function comme ceci :

def utility():
# do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

Alors, dans une autre fonction, vous pouvez accéder à la fonction utility() comme ceci :

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
utility = functions['My Function']['utility']
...

Vous pouvez aussi utiliser les objets fonctions pour stocker des données persistantes, qui peuvent être ré-utilisées par d’autres fonctions. Par exemple, vous pourriez avoir une fonction qui lorsqu’elle s’exécute avec Remplacer Tout collecte des données et une autre fonction qui les utilisera lorsqu’elle sera lancée plus tard. Considérez les deux fonctions suivantes :

# 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']
...

Dépanner vos fonctions

Vous pouvez dépanner les fonctions que vous créez en utilisant la fonction standard print() de Python. Le résultat de print sera affiché dans une fenêtre popup après que le Rechercher/remplacer soit terminé. Nous avons vu un exemple de l’utilisation de print() pour sortir une table des matières entière plus haut.

Choisissez l’ordre de fichier lors de l’exécution sur de multiples fichiers HTML

Lorsque vous lancez Remplacer tout sur de multiples fichiers HTML, l’ordre dans lequel les fichiers sont traités dépend de quels fichiers vous avez ouvert pour l’édition. Vous pouvez forcer la recherche à traiter les fichiers dans l’ordre dans lequel ils apparaissent en paramétrant l’attribut file_order de votre fonction, comme ceci :

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.file_order = 'spine'

file_order accepte deux valeurs, spine et spine-reverse qui entraîne le traitement de multiples fichiers dans l’ordre où ils apparaissent dans le livre, soit descendant soit ascendant, respectivement.

Appelle votre fonction une dernière fois après que la dernière correspondance ait été trouvée.

Parfois, comme dans l’exemple de la table des matières auto-générée ci-dessus, il est utile que votre fonction soit appelée une nouvelle fois après que la dernière correspondance ait été trouvée. Vous pouvez faire ceci en paramétrant l’attribut call_after_last_match dans votre fonction, comme ceci :

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.call_after_last_match = True

Ajouter le résultat de la fonction au texte marqué

Lorsque vous effectuez un rechercher et remplacer sur du texte marqué, il est quelquefois utile d’ajouter du texte à la fin du texte marqué. Vous pouvez faire cela en paramétrant l’attribut append_final_output_to_marked sur votre fonction (notez que vous aurez également besoin de paramétrer ``call_after_last_match`), comme ceci :

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

Suppression du dialogue résultant lorsque des recherches sont effectuées sur du texte marqué

Vous pouvez également supprimer le résultat du dialogue (qui peut ralentir l’application répétée d’un rechercher/remplacer sur beaucoup de blocs de texte) en paramétrant l’attribut suppress_result_dialog sur votre fonction, comme ceci :

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.suppress_result_dialog = True

Plus d’exemples

Plus d’exemples utiles, fournis par des utilisateurs de calibre, peuvent être trouvés dans le Forum Éditeur de livre numérique de calibre [https://www.mobileread.com/forums/showthread.php?t=237181].

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Éditer des livres numériques »
- Fragments

Fragments

L’éditeur de livre numérique de calibre supporte les fragments. Un fragment est un morceau de texte qui est soit souvent réutilisé soit contient beaucoup de texte redondant. L’éditeur vous permet d’insérer un fragment avec seulement quelques séquences de touches. Par exemple, supposons que vous devez souvent trouver par vous même les balises link insérées lors de l’édition des fichiers HTML, alors vous pouvez simplement taper <a dans l’éditeur et presser Control+J. L’éditeur développera cela en

<a href="filename"></a>

Pas seulement ça, le mot filename sera sélectionné, avec le curseur placé sur lui, ainsi vous pouvez facilement taper le vrai nom de fichier, en utilisant la petite fonctionnalité de l’éditeur Auto-complétion. Et une fois que vous avez fini de taper le nom de fichier, pressez Control+J à nouveau et le curseur sautera vers la position entre les balises <a> comme cela vous pouvez facilement y taper le texte pour le lien.

Le système des fragments dans l’éditeur est très sophistiqué, il y a quelques fragments intégrés et vous pouvez créer les vôtres pour convenir à votre style d’édition.

L’exposé qui suit sur les fragments intégrés devrait aider à illustrer la puissance du système des fragments.

Note

Vous pouvez aussi utiliser les fragments dans les champs d’entrée de texte dans le panneau Rechercher & Remplacer, cependant, les espaces réservés (en utilisant Control+J pour s’y déplacer) ne fonctionneront pas.

Les fragments intégrés

Les fragments intégrés sont décrits ci-dessous . Notez que vous pouvez les remplacer en créant vos propres fragments avec le même texte déclencheur.

Insertion du texte de remplissage [Lorem]

Le premier fragment intégré, et le plus simple, est utilisé pour insérer du texte de remplissage dans un document. Le texte de remplissage est est issu de De finibus bonorum et malorum [https://fr.wikipedia.org/wiki/De_finibus_bonorum_et_malorum] une oeuvre philosophique de Cicéron (Traduite en anglais). Pour l’utiliser, taper simplement Lorem dans un fichier HTML et pressez Control+J. Il sera remplacé par quelques paragraphes de remplissage.

La définition de ce fragment est très simple, le texte déclencheur est défini comme étant Lorem et le modèle est défini simplement comme le texte littéral à intégrer. Vous pouvez facilement le personnaliser pour utiliser votre texte de remplissage favori.

Insérer une balise HTML auto-fermante [<>]

Maintenant jetons un oeil à un exemple simple du puissant concept de placeholders. Disons que vous voulez insérer une balise auto-fermante <hr/>. Tapez juste <> et pressez Control+J, l’éditeur développera le fragment en

<|/>

Ici, le symbole | représente la position actuelle du curseur. Vous pouvez alors taper hr et presser Control+J pour déplacer le curseur après la fin de la balise. Ce fragment est défini comme

Trigger: <>
Template: <$1/>$2

Les espaces réservés sont simplement le signe dollar ($) suivi par un nombre. Lorsque le fragment est développé en appuyant sur Control+J le curseur est positionné au premier espace réservé (l’espace réservé avec le nombre le plus bas). Quand vous pressez Control+J à nouveau le curseur saute vers l’espace réservé suivant (l’espace réservé suivant avec le nombre plus élevé).

Insérer une balise lien HTML [<a]

Les balises lien HTML partagent toutes une structure commune. Elles ont un attribut href et un certain texte entre les balises ouvrantes et fermantes. Un fragment pour rendre la frappe plus efficiente nous introduira certaines fonctionnalités supplémentaires des espaces réservés. Pour utiliser ce fragment, tapez simplement <a et appuyez Control+J. L’éditeur développera ceci en

<a href="filename|"></a>

Pas seulement ça, le mot filename sera sélectionné, avec le curseur placé sur lui, ainsi vous pouvez facilement taper le vrai nom de fichier, en utilisant la petite fonctionnalité de l’éditeur Auto-complétion. Et une fois que vous avez fini de taper le nom de fichier, pressez Control+J à nouveau et le curseur sautera vers la position entre les balises <a> comme cela vous pouvez facilement y taper le texte pour le lien. Après que vous ayez fini de taper le texte, appuyez Control+J encore une fois pour sauter au point après la balise fermante. Le fragment est défini comme

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

Il y a ici deux nouvelles fonctionnalités. Premièrement l’espace réservé $1 est devenu plus complexe. Il inclut maintenant un certain texte par défaut (le mot filename). Si un espace réservé contient du texte par défaut , le texte par défaut est substitué pour l’espace réservé quand le fragment est développé. Aussi quand vous sautez vers un espace réservé avec du texte par défaut en utilisant Control+J, le texte par défaut est sélectionné. De cette manière, vous pouvez utiliser le texte par défaut comme un rappel pour vous pour remplir d’importantes parties du modèle. Vous pouvez définir le texte par défaut pour un espace réservé en utilisant la syntaxe : ${<number>:texte par défaut}.

L’autre nouvelle fonctionnalité est que le second espace réservé a un astérisque qui le suit (${2*}). Cela signifie que tout texte qui est sélectionné avant de développer le modèle est substitué pour l’espace réservé. Pour voir ceci en action, sélectionner un certain texte dans l’éditeur, appuyer Control+J, tapez <a et pressez Control+J à nouveau, le modèle sera développé en

<a href="filename">whatever text you selected</a>

Insérer une balise image HTML [<i]

C’est très similaire à insérer un lien HTML, comme nous avons vu ci-dessus. Cela vous permet d’entrer rapidement une balise <img src="filename" alt="description" /> et sauter entre les attributs src et alt

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

Insérer une balise HTML quelconque[<<]

Ceci vous permet d’insérer une balise HTML quelconque (ou d’envelopper le texte précédemment sélectionné dans la balise). Pour l’utiliser, tapez simplement << et pressez Control+J. L’éditeur développera cela en:

<|></>

Tapez le nom de la balise, par exemple : span et pressez Control+J, ce qui résultera en

<span>|</span>

Vous noterez que la balise fermante a été automatiquement remplie avec span. Ceci est accompli avec encore une autre fonctionnalité des espaces réservés, mirroring. Mirroring signifie simplement que si vous spécifiez plus d’une fois l’échantillon d’espace réservé dans le modèle, le second et et toutes les positions futures seront automatiquement remplies avec ce que vous avez tapé en première position, quand vous appuyez sur Control+J. La définition pour ce fragment est

Trigger: <<
Template: <$1>${2*}</$1>$3

Comme vous pouvez voir, le premier espace réservé ($1) a été spécifié deux fois, la deuxième fois dans la balise fermante, ce qui copiera simplement ce que vous aurez tapé dans la balise ouvrante.

Insérer une balise HTML quelconque avec un attribut de classe [<c]

Ceci est très similaire à l’exemple d’insertion d’une balise quelconque ci-dessus, exceptez qu’il assume que vous voulez spécifier une classe pour la balise

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

Ceci vous permettra de taper d’abord le nom de la balise, presser Control+J, taper le nom de la classe, presser Control+J tapez le contenu de la balise et pressez Control+J une dernière fois pour sortir de la balise. La balise de fermeture sera remplie automatiquement.

Créer vos propres fragments

Les fragments sont vraiment brillants car vous pouvez créer les vôtres pour suivre votre style d’édition. Pour créer vos propres fragments allez à Éditer → Préférences → Propriétés de l’éditeur → Gérer les fragments dans l’éditeur. Cela fera apparaître une boîte de dialogue facile à utiliser pour vous aider à créer vos propres fragments. Cliquez simplement le bouton :guilabel:`Ajouter un fragment`et vous verrez une boîte de dialogue qui ressemble à :

Tout d’abord donnez un nom à votre fragment, quelque chose de descriptif, pour aider à identifier le fragment par après. Spécifiez alors le déclencheur. Un déclencheur est simplement le texte que vous avez à taper avant de presser Control+J en vue de développer le fragment.

Spécifiez alors le modèle de fragment. Vous devriez commencer avec l’un des exemples ci-dessus et le modifier pour correspondre à vos besoins. Finalement, spécifier quels types de fichiers pour lesquels vous voulez que le fragment soit actif. De cette manière vous pouvez avoir de multiples fragments avec le même texte déclencheur qui fonctionne différemment dans différents types de fichiers.

L’étape suivante est de tester votre fragment nouvellement créé. Utilisez la boîte Test en bas. Entrez-y le texte déclencheur et appuyez sur Control+J pour développer le fragment et sauter entre les espaces réservés.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Le Serveur de contenu calibre

Le Serveur de contenu calibre

Le :guilabel:`Serveur de contenu`calibre vous permet d’accéder à vos bibliothèques calibre et de lire des livres directement dans un navigateur sur votre smartphone ou votre tablette. De ce fait, vous n’avez pas besoin d’installer une application de lecture ou de gestion de livres sur votre téléphone. Utilisez simplement le navigateur. Le serveur télécharge et conserve le livre que vous lisez dans un cache hors ligne afin que vous puissiez le lire même quand il n’y a pas de connexion internet.

Contenu

- Accéder au Serveur de contenu depuis d’autres périphériques

- Accès au serveur à partir de périphériques sur votre réseau domestique

- Accès au serveur depuis n’importe où sur Internet

- L’interface du serveur

- La liste de livres

- Le lecteur de livre

- Compatibilité du navigateur

- Activer le support hors ligne

- Gérer les comptes utilisateurs uniquement à partir de la ligne de commande

- Intégration du Serveur de contenu calibre dans d’autres serveurs

- Utilisation d’un hôte totalement virtuel

- Utilisation d’un préfixe d’URL

- Créer un service pour le serveur calibre sur un système Linux moderne

Pour démarrer le serveur, cliquez le bouton Connecter/partager`et choisissez :guilabel:`Démarrer le Serveur de contenu. Vous pourriez avoir un message du pare-feu ou de l’antivirus de votre ordinateur demandant si vous voulez autoriser l’accès à calibre.exe. Cliquez le bouton Autoriser ou OK. Ouvrez alors un navigateur (de préférence Chrome ou Firefox) sur votre ordinateur et entrez l’adresse suivante :

http://127.0.0.1:8080

Ceci ouvrira une page dans dans votre navigateur vous affichant vos bibliothèques calibre, cliquez sur n’importe laquelle et parcourez les livres qu’elle contient. Cliquez sur un livre, et il vous montrera toutes les métadonnées à propos du livre, accompagnées des boutons Lire et Télécharger le livre. Cliquez sur le bouton Lire pour commencer la lecture du livre.

Note

L’adresse utilisée ci-dessus http://127.0.0.1:8080 ne fonctionnera que sur l’ordinateur qui fait tourner calibre. Pour accéder au serveur à partir d’autres ordinateurs, téléphones, tablettes, etc… il va falloir un peu plus de travail, comme indiqué dans la section suivante.

Accéder au Serveur de contenu depuis d’autres périphériques

Il existe deux types d’accès depuis un périphérique distant dont vous aurez généralement besoin. Le premier type, plus simple, est l’accès depuis votre réseau domestique. Si vous exécutez calibre sur un ordinateur de votre réseau domestique et que vous avez également connecté vos autres périphériques au même réseau, vous devriez pouvoir accéder facilement au serveur sur ces appareils.

Accès au serveur à partir de périphériques sur votre réseau domestique

Après avoir démarré le serveur dans calibre comme décrit ci-dessus, cliquez à nouveau sur le bouton Connecter/partager. Au lieu de l’action Démarrer le serveur de contenu, vous devriez voir l’action Arrêter le Serveur de contenu. À droite de cette action il y a une adresse IP et un numéro de port. Cela ressemble à un tas de nombres séparés par deux points. Par exemple

Stop Content server [192.168.1.5, port 8080]

Ces chiffres indiquent l’adresse à utiliser pour se connecter au serveur depuis vos périphériques. En suivant l’exemple ci-dessus, l’adresse devient

http://192.168.1.5:8080

La première partie de l’adresse est toujours http:// la partie suivante est l’adresse IP, c’est à dire les chiffres avant la virgule, et pour terminer le numéro de port qui doit être ajouté à l’adresse IP avec deux points (:). Si vous êtes chanceux, c’est tout ce dont vous devriez avoir besoin pour explorer vos bibliothèques calibre sur votre périphérique. Sinon, continuez la lecture.

Dépannage de la connexion sur un réseau domestique

Si vous ne parvenez pas à accéder au serveur depuis votre périphérique, essayez ce qui suit :

1. Vérifiez que le serveur fonctionne en ouvrant l’adresse http://127.0.0.1:8080 dans un navigateur fonctionnant sur le même ordinateur que le serveur.

2. Vérifiez que votre pare-feu/anti-virus autorise les connexions à votre ordinateur sur le port 8080 et au programme calibre. La façon la plus simple de savoir si le pare-feu/anti-virus sont la source du problème est de les désactiver temporairement puis d’essayer de vous connecter. Avant d’éteindre le pare-feu, commencez par vous déconnecter d’Internet pour garder votre ordinateur en sécurité.

3. Vérifiez que votre périphérique et votre ordinateur sont sur le même réseau. Cela signifie qu’ils doivent être tous les deux connectés au même routeur sans fil. En particulier aucun ne doit être en train d’utiliser une connexion cellulaire ou une connexion directe WiFi fournie par votre FAI.

4. Si votre configuration réseau n’est pas standard, il se peut que l’adresse IP affichée dans le menu Connecter/partager soit incorrecte. Dans ce cas, vous devrez déterminer l’adresse IP correcte à utiliser par vous-même. Malheureusement, compte tenu de la grande diversité des configurations réseau possible, il est impossible de donner un mode d’emploi général.

5. Si vous avez configuré un nom d’utilisateur et un mot de passe, essayez tout d’abord sans pour voir s’il cela pause problème. Certaines périphériques e-ink ont des navigateurs qui ne prennent pas en charge l’authentification. Vous pouvez parfois contourner cela en incluant le nom d’utilisateur et le mot de passe dans l’URL, par exemple : http://nomutilisateur:motdepasse@192.168.1.2:8080.

6. Si vous êtes coincé, vous pouvez toujours demander de l’aide sur les forums utilisateurs de calibre [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Accès au serveur depuis n’importe où sur Internet

Avertissement

Avant de commencer, vous devriez activer la protection nom d’utilisateur/mot de passe dans le serveur, sinon n’importe qui dans le monde pourra accéder à vos livres. Allez dans Préférences →  Partager →  Partager sur le net et activez l’option Exiger un nom d’utilisateur et un mot de passe pour accéder au serveur de contenu.

Bien que les détails pour la configuration de l’accès via Internet varient en fonction de la configuration du réseau et du type d’ordinateur que vous utilisez, la méthode de base est la suivante.

1. Trouvez l’adresse IP externe de l’ordinateur sur lequel vous allez exécuter le serveur. Vous pouvez l’obtenir en visitant le site What is my IP address [https://www.whatismyip.com/] dans un navigateur sur l’ordinateur.

2. Si l’ordinateur est derrière un routeur, activez le transfert de port (ou “port forwarding”) sur le routeur pour transférer le port 8080 (ou n’importe quel port sur lequel vous choisissez d’exécuter le serveur de contenu calibre) sur l’ordinateur.

3. Assurez-vous que le serveur calibre est autorisé par tout programme pare-feu/anti-virus sur votre ordinateur.

4. Maintenant, vous devriez pouvoir accéder au serveur depuis n’importe quel périphérique connecté à Internet en utilisant l’adresse IP obtenue lors de la première étape. Par exemple, si l’adresse IP obtenue était 123.123.123.123 et que le port que vous utilisez pour le serveur calibre est 8080, l’adresse à utiliser sur votre périphérique devient: http://123.123.123.123:8080.

5. Accessoirement, vous pouvez utilisez un service comme no-ip [https://www.noip.com/free] pour mettre en place une adresse facile à retenir à utiliser à la place de l’adresse IP obtenue à la première étape.

Note

Pour une sécurité maximum, vous devriez aussi activer l’HTTPS sur le serveur de contenu. Vous pouvez soit le faire directement dans le serveur en fournissant le chemin vers le certificat HTTPS à utiliser dans les configurations avancées du serveur, ou vous pouvez configurer un proxy inversé (reverse proxy) comme décris ci-dessous, pour utiliser une configuration HTTP déjà existante.

L’interface du serveur

L’interface du serveur est une version simplifiée de l’interface principale de calibre, optimisée pour l’utilisation d’écrans tactiles. L’écran d’accueil vous affiche les livres que vous êtes en train de lire aussi bien que de vous permettre de choisir une bibliothèque calibre que vous voulez parcourir. Le serveur, dans calibre, vous donne accès à toutes vos bibliothèques, pas seulement à une seule, comme précédemment.

La liste de livres

La liste de livres du serveur est une simple grille de couvertures. Appuyez sur une couverture pour voir les métadonnées détaillées pour un livre, ou pour lire un livre. Si vous préférez une liste plus détaillée, vous pouvez changer la vue par défaut en cliquant sur les trois points verticaux dans le coin supérieur droit.

Trier et rechercher la liste de livres devrait être familier aux utilisateurs de calibre. Ils peuvent être accédés en cliquant sur leurs icônes dans la partie supérieure droite. Ils fonctionnent tous deux exactement de la même manière que dans le programme principal calibre. La page de recherche vous permet même de construire des requêtes en cliquant sur auteurs/étiquettes/etc., exactement comme vous le faites en utilisant le Navigateur d’étiquettes du programme principal.

Une fonctionnalité très appréciée du programme principal, Bibliothèques virtuelles est aussi présente dans l’interface du serveur. Cliquez sur les trois points verticaux dans le coin supérieur doit pour choisir une Bibliothèque virtuelle.

Le lecteur de livre

Vous pouvez lire n’importe quel livre dans votre bibliothèque calibre en appuyant simplement dessus et sélectionnant ensuite le bouton Lire. Le lecteur de livres est très simple de fonctionnement. Vous pouvez à la fois appuyer et glisser pour tourner les pages. Glisser vers le haut/bas bascule entre les chapitres. Appuyer le quart supérieur de l’écran vous donne des contrôles détaillés et des préférences de visionneuse.

Si vous laissez le Serveur de contenu en fonctionnement, vous pouvez même ouvrir le même livre sur de multiples appareils et il se souviendra de votre dernière position de lecture. S’il ne le fait pas vous pouvez forcer une synchronisation en appuyant dans le quart supérieur et en choisissant Sync.

Compatibilité du navigateur

Le nouveau serveur calibre fait un usage intensif des fonctionnalités avancées de HTML 5 et CSS 3. En tant que tel, il nécessite un navigateur à jour pour être utilisé. Il a été testé sur Android Chrome et iOS Safari ainsi que sur Chrome et Firefox sur le bureau.

Le serveur est attentif à utiliser les fonctionnalités qui ont soit déjà été standardisées soit sur les voies de la standardisation. En tant que tel s’il ne fonctionne pas actuellement avec votre navigateur, il le fera probablement une fois que ce navigateur aura rattrapé son retard.

Si vous utilisez un navigateur particulièrement ancien ou limité ou que vous n’aimez pas exécuter JavaScript, vous pouvez utiliser la vue « mobile », en ajoutant simplement /mobile à l’adresse du serveur.

Note

Sous IOS, Apple permet un seul moteur de navigation, donc Firefox, Chrome et Safari sont actuellement le même navigateur sous le capot. La nouvelle interface serveur requiert IOS 10.3.2 ou plus récent. Sous Android, le serveur a été testé avec Chrome 58 et plus récent.

Activer le support hors ligne

Les fabricants de navigateurs ont essayé de forcer les gens à utiliser SSL en désactivant les fonctions avancées de leur navigateur pour les connexions HTTP simples. L’une de ces victimes est ApplicationCache, qui a été utilisé dans calibre pour l’assistance hors ligne. Par conséquent, aujourd’hui, malheureusement, le mode hors ligne ne fonctionne que si vous gardez l’onglet du navigateur ouvert. De plus, dans Firefox sous Android, vous devrez taper about:config et rechercher browser.tabs.useCache et le définir sur true.

Gérer les comptes utilisateurs uniquement à partir de la ligne de commande

Le programme calibre a une belle section dans Préférences pour vous permettre de gérer les comptes utilisateurs pour le serveur. Si vous voulez exécuter le serveur autonome et ne pouvez exécuter le programme principal calibre sur le même ordinateur/compte utilisateur, vous pouvez également gérer les utilisateurs en utilisant uniquement la ligne de commande.

Vous pouvez gérer les comptes utilisateur en utilisant l’option --manage-users sur le programme calibre-server autonome. Supposez que vous voulez stocker la base de données utilisateur dans le répertoire /srv/calibre, alors créez-le en exécutant

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

Suivez juste les instructions pour créer les comptes utilisateur, définir leurs permissions, etc. Une fois que vous avez fini, vous pouvez exécuter le serveur en tant que

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

Il utilisera les comptes utilisateur que vous avez créé à l’étape précédente.

Intégration du Serveur de contenu calibre dans d’autres serveurs

Ici, nous allons vous montrer comment intégrer le serveur de contenu calibre dans un autre serveur. La raison la plus fréquente de faire cela est d’utiliser SSL ou de desservir la bibliothèque calibre comme partie d’un site plus grand. La technique de base consiste à exécuter le serveur calibre et à configurer un proxy inversé à partir du serveur principal.

Un proxy inversé est quand votre serveur normal accepte les requêtes entrantes et les transmet au serveur calibre. Il lit alors la réponse du serveur calibre et la transmet au client. Ceci signifie que vous pouvez simplement exécuter le serveur calibre comme à la normale sans essayer de l’intégrer étroitement avec votre serveur principal.

Utilisation d’un hôte totalement virtuel

La configuration la plus simple est de dédier un hôte totalement virtuel au serveur calibre. Dans ce cas, exécuter le serveur calibre comme ceci:

calibre-server

Maintenant configurer l’hôte virtuel dans votre serveur principal, par exemple pour 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;
}
}

Ou, pour 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>

Utilisation d’un préfixe d’URL

Si vous ne voulez pas dédier un hôte totalement virtuel à calibre. vous pouvez l’avoir pour utiliser un préfixe d’URL. Démarrer le serveur calibre comme ceci:

calibre-server --url-prefix /calibre --port 8080

Ici le paramètre clé est --url-prefix/calibre. Ceci entraîne le Serveur de contenu à servir toutes les URLs préfixées par /calibre. Pour voir ceci en action, visitez http://localhost:8080/calibre dans votre navigateur. Vous devriez voir le site web normal du Serveur de contenu, mais maintenant il s’exécutera sous /calibre.

Avec nginx, la configuration nécessaire est

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;
}

Pour Apache, activer tout d’abord les modules proxy dans Apache, en ajoutant ce qui suit à httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

La technique exacte pour activer les modules proxy variera selon votre installation Apache. Une fois que vous avez activé les modules proxy, ajouter les règles suivantes à httpd.conf (ou si vous utilisez des hôtes virtuels au fichier conf pour l’hôte virtuel en question):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

C’est tout, vous serez maintenant capable d’accéder au Serveur de contenu calibre sous l’URL /calibre dans votre serveur principal. Les règles ci-dessus transitent toutes les requêtes sous /calibre vers le serveur calibre s’exécutant sur le port 8080 et grâce à l’option --url-prefix ci-dessus, le serveur calibre les traite de manière transparente.

Note

Lors de l’utilisation d’un proxy inversé, vous devrez dire au Serveur de contenu calibre d’écouter uniquement le localhost, en utilisant --listen-on 127.0.0.1. De cette manière, le serveur écoutera uniquement les connexions venant du même ordinateur, càd du proxy inversé.

Note

Si vous avez configuré SSL pour votre serveur principal, vous devrez dire au serveur calibre d’utiliser l’authentification basique au lieu de l’authentification digest, plus rapide. Pour faire cela, transmettez l’option --auth-mode=basic à calibre-server.

Créer un service pour le serveur calibre sur un système Linux moderne

Vous pouvez aisément créer un service pour exécuter le serveur calibre sur sur un système moderne (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) basé sur Linux. Il faut seulement créer le fichier /etc/systemd/system/calibre-server.service avec le contenu afficher ci-dessous

[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

Changer mylinuxuser et mylinuxgroup en n’importe quel utilisateur et groupe sous lesquels vous désirez que le serveur tourne. Ceci devrait être le même utilisateur et groupe que ceux qui possèdent les fichiers dans le répertoire de la bibliothèque calibre. Notez que ce n’est généralement pas une bonne idée d’exécuter le serveur en tant que root. Changer également le chemin vers le répertoire de la bibliothèque calibre pour qu’il convienne à votre système. Vous pouvez ajouter de multiples bibliothèques si nécessaire. Voir l’aide pour la commande calibre-server.

Maintenant exécutez

sudo systemctl start calibre-server

pour démarrer le serveur. Vérifiez son statut avec

sudo systemctl status calibre-server

Pour le faire démarrer au boot, exécutez

sudo systemctl enable calibre-server

Note

Le serveur calibre ne nécessite pas un serveur X en cours d’exécution, mais il nécessite que les librairies X soient installées.

Note

Le serveur calibre supporte également l’activation du socket systemd, vous pouvez donc l’utiliser, si nécessaire, bien sûr.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Comparer des livres numériques

Comparer des livres numériques
calibre inclut un outil intégré de comparaison de livres numériques qui peut être utilisé pour voir ce qui a été changé à l’intérieur d’un livre numérique après son édition ou sa conversion. Il peut comparer des livres aux formats EPUB et AZW3.

Pour l’utiliser, soit ouvrez le livre dans l’outil Éditer et cliquez alors Fichier → Comparer à un autre livre ou utilisez le panneau Détails du livre. Si vous faites une conversion d’EPUB à EPUB, l’EPUB original sera sauvegardé comme ORIGINAL_EPUB. Faites simplement un clic droit sur l’entrée ORIGINAL_EPUB dans le panneau Détails du livre et choisissez Comparer au format EPUB.

L’outil de comparaison qui s’ouvre ressemblera à la capture d’écran ci-dessous. Il vous montre les différences dans le texte, les styles et le images dans les livres choisis.

Comprendre la vue de comparaison

Comme cela peut être vu dans la capture d’écran plus haut, la vue de comparaison montre les différences entre les deux livres côte à côte. Seules les différences, avec quelques lignes de contexte autour d’elles sont montrées. Ceci rend facile de voir en un coup d’œil seulement ce qui a été changé à l’intérieur d’un grand document comme un livre.

Le texte ajouté est montré avec un fond vert, le texte supprimé avec un fond rouge et le texte changé avec un fond bleu.

Les numéros de ligne de tout texte changé sont montrés sur les côtés, rendant facile le déplacement à un changement particulier dans l’éditeur. Quand vous ouvrez l’outil de comparaison depuis l’éditeur, vous pouvez aussi double-cliquer sur une ligne dans le panneau de droite pour aller automatiquement à cette ligne dans l’éditeur.

Une technique utile en comparant des livres est de dire à l’outil de comparaison d’embellir les fichiers de texte et de style avant de calculer les différences. Ceci peut souvent avoir comme conséquence d’obtenir des résultats plus propres et plus de facilité à suivre les différences. Pour faire ceci, cliquez sur le bouton Options en bas à droite et choisir Embellir les fichiers avant de comparer. Notez que l’embellissement peut parfois avoir des effets non désirés, comme altéré un balisage invalide pour le rendre valide. Vous pouvez également changer le nombre de lignes de contexte autour des différences via le bouton guilabel:Options.

Vous pouvez chercher après n’importe quel texte dans les différences par l’intermédiaire de la Barre de recherche en bas. Vous aurez besoin de spécifier dans quel panneau rechercher, le Gauche ou le Droit.

Lancement de l’outil de comparaison

L’outil de comparaison est le plus utile quand vous avez deux versions du mêmes livre et que vous voulez voir ce qui est différent entre elles. À cet effet, il y a plusieurs manières de lancer l’outil.

Comparer deux fichiers de livre numérique

Ouvrez le premier fichier dans l’outil Éditer des livres numériques. Maintenant cliquez Fichier → Comparer à un autre livre et choisissez le deuxième livre. (il doit être dans le même format que le premier). La vue comparaison s’ouvrira avec le fichier en cours d’édition sur la droite et le second fichier sur la gauche.

Comparaison de ORIGINAL_FMT à FMT

Quand vous faites une conversion dans calibre à partir d’un FMT vers lui-même, le fichier original est sauvegardé comme ORIGINAL_FMT. Vous pouvez voir ce qui a été changé par la conversion, en faisant un clic droit sur l’entrée ORIGINAL_FMT dans le panneau ref:book_details dans la fenêtre principale de calibre et en sélectionnant Comparer à FMT. La vue comparaison s’ouvrira avec le ORIGINAL_FMT sur la gauche et le FMT sur la droite.

Comparer un point de contrôle à l’état actuel du livre tout en l’éditant

L’outil edition possède une caractéristique très utile points de contrôle. Ceci vous permet de sauver l’état actuel du livre comme un point de contrôle nommé, auquel vous pouvez retourner si vous n’aimez pas les modifications que vous avez apportées depuis la création du point de contrôle. Des points de contrôle sont également créés automatiquement quand vous effectuez diverses actions automatisées dans l’éditeur. Vous pouvez voir la liste de points de contrôle en allant dans Aperçu → Points de contrôle et employez alors le bouton Comparer pour comparer le livre au point de contrôle sélectionné à l’état actuel. L’outil de comparaison montrera le point de contrôle sur la gauche et la version actuelle sur la droite.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Éditer les métadonnées d’un livre numérique

Éditer les métadonnées d’un livre numérique

Contenu

- Édition des métadonnées d’un livre à la fois

- Télécharger les métadonnées

- Gérer les formats de livre

- Tout à propos des couvertures

- Édition des métadonnées de plusieurs livres à la fois

- Rechercher Et Remplacer

- Téléchargement de métadonnées par lot

- Ajouter des fichiers de données supplémentaires à un livre

Les livres numériques nous parviennent sous toutes les formes et tailles, et le plus souvent leurs métadonnées (éléments comme le titre/auteur/séries/éditeur) sont incomplètes ou incorrectes. La manière la plus simple de changer des métadonnées dans calibre est de simplement double cliquer sur une entrée et de saisir la correction. Plus sophistiquée, « édition puissante » utilise les outils d’édition de métadonnées discutés ci-dessous.

Édition des métadonnées d’un livre à la fois

Cliquez sur le livre que vous voulez éditer et cliquez alors sur le bouton Modifier les métadonnées ou appuyez sur la touche E. Une boîte de dialogue s’ouvre vous permettant d’éditer tous les aspects des métadonnées. Il y a différentes fonctionnalités pour rendre l’édition plus rapide et plus efficiente. Une liste des astuces habituellement utilisées :

- Vous pouvez cliquer le bouton entre titre et auteurs pour les permuter automatiquement.

- Vous pouvez cliquer le bouton à coté de Tri par auteur pour faire en sorte que calibre le remplisse automatiquement en utilisant le tri des valeurs stockées avec chaque auteur. Utilisez la boîte de dialogue Gérer les auteurs pour voir et changer les valeurs de tri des auteurs. Cette boîte de dialogue peut être ouverte en cliquant longuement sur le bouton à coté de Tri par auteur.

- Vous pouvez cliquer sur le bouton qui précède les étiquettes pour utiliser :quilabel:`Gestion des étiquettes` pour gérer les étiquettes associées avec le livre.

- Les cases « ids » peuvent être utilisées pour entrer un numéro d’ISBN (et beaucoup d’autres types d’id), elles auront un arrière plan rouge si vous entrez un ISBN invalide. Il sera vert pour les ISBN valides.

- La case Tri par auteur sera rouge si la valeur de tri par auteur diffère de ce que calibre pense qu’elle devrait être.

Télécharger les métadonnées

La plus agréable fonctionnalité de la boîte de dialogue d’édition des métadonnées est sa capacité à remplir automatiquement beaucoup de champs de métadonnées en obtenant les métadonnées de divers sites web. Actuellement, calibre utilise Google Books et Amazon. Le téléchargement de métadonnées peut compléter Titre, Auteur, Série, Étiquettes, Notation, Description et ISBN pour vous.

Pour utiliser le téléchargement, complétez les champs titre et auteur et cliquez sur le bouton Télécharger les métadonnées. calibre vous présentera une liste des livres qui correspondent le plus précisément au titre et à l’auteur. Si vous complétez l’ISBN en premier, il sera utilisé en priorité sur le titre et l’auteur. Si aucune correspondance n’est trouvée, essayez de rendre votre recherche un peu moins spécifique en incluant seulement quelques mots clés dans le titre et seulement le nom de l’auteur.

Gérer les formats de livre

Dans calibre, une seule entrée de livre peut avoir beaucoup de formats associés. Par exemple vous avez pu obtenir les Œuvres de Shakespeare au format EPUB et l’avoir converti plus tard en MOBI pour lire sur votre Kindle. calibre gère automatiquement les multiples formats pour vous. Dans la section Formats disponibles de la boîte de dialogue Éditer les métadonnées, vous pouvez gérer ces formats. Vous pouvez ajouter un nouveau format, supprimer un format existant et aussi demander à calibre de régler les métadonnées et la couverture pour l’entrée de livre depuis les métadonnées dans l’un des formats.

Tout à propos des couvertures

Vous pouvez demander à calibre de télécharger les couvertures de livre pour vous, si le livre a un ISBN connu. Alternativement, vous pouvez spécifier un fichier sur votre ordinateur à utiliser comme couverture. calibre peut même générer une couverture par défaut pourvue de métadonnées basiques pour vous. Vous pouvez glisser et déposer des images sur la couverture pour la changer et aussi faire un clic droit pour copier/coller des images de couverture.

En complément, il y a un bouton pour ajuster automatiquement les bords de la couverture, au cas où votre image de couverture a un vilain bord.

Édition des métadonnées de plusieurs livres à la fois

Sélectionnez d’abord les livres que vous voulez éditer en maintenant Ctrl ou Shift et en les cliquant. si vous sélectionnez plus d’un livre, cliquez le bouton Éditer les metadonées entraînera l’ouverture de la boîte de dialogue d’édition de métadonnées par lot. En utilisant cette boîte de dialogue, vous pouvez rapidement régler l’auteur/l’éditeur/la notation/les étiquettes/ les séries etc. d’un tas de livres à la même valeur. Ceci est particulièrement utile si vous venez juste d’importer un nombre de livres qui ont des métadonnées en commun. Cette boîte de dialogue est très puissante, par exemple, elle a un onglet :guilable:`Rechercher et remplacer` que vous pouvez utiliser pour effectuer des opérations par lot sur les métadonnées et même copier des métadonnées d’une colonne à un autre.

La boite de dialogue normale d’édition des métadonnées a aussi des boutons Suivant et Précédent que vous pouvez utiliser pour éditer les métadonnées de plusieurs livres l’un après l’autre.

Rechercher Et Remplacer

La boîte de dialogue Modifier les métadonnées de nombreux livres vous permet d’effectuer des opérations de recherche et de remplacement arbitrairement puissante sur les livres sélectionnés. Elle utilise par défaut un rechercher et remplacer en texte simple, mais elle supporte aussi les expressions régulières. Pour plus d’informations sur les expressions régulières, voir Tout à propos de l’utilisation des expressions régulières dans calibre.

Comme noté plus haut, il y a deux modes de rechercher et remplacer : la correspondance de caractère et l’expression régulière. La correspondance de caractère regardera dans le champ de recherche que vous choisissez après les caractères que vous avez entré dans la case Rechercher et remplace ces caractères par ceux que vous avez entré dans la case Remplacer par. Chaque occurrence des caractères recherchés dans le champ sera remplacée. Par exemple, supposons que le chant faisant l’objet de la recherche contient a bad cat, si vous recherchez après un a à remplacer par HELLO, alors le résultat sera HELLO bHELLOd cHELLOt.

Si le champ qui fait l’objet de la recherche est un champ multiple comme étiquettes, alors chaque étiquette est traitée séparément. Par exemple, si vos étiquettes contiennent Horreur, Effrayant, l’expression de recherche `r, ne correspondra pas à quelque chose parce que l’expression sera d’abord appliquée à Horreur et ensuite à Effrayant.

Si vous voulez que la recherche ignore les différences majuscules/minuscules, décochez la case Sensible à la casse.

Vous pouvez faire en sorte que calibre change la casse du résultat (l’information après le remplacement s’est produite) en choisissant l’une des fonctions de la case Casse à appliquer après le remplacement. Les opérations disponibles sont :

- Minuscule – changer tous les caractères dans le champ en minuscule

- Majuscule – changer tous les caractères dans le champ en majuscule

- Casse de titre – met une majuscule à chaque mot du résultat.

La case Votre test vous est procurée pour entrer du texte pour vérifier que rechercher/remplacer fait ce que vous voulez. Dans la majorité des cas la case de test livre sera suffisante, mais il est possible y a un cas que vous voulez vérifier qui n’apparaît pas dans ces cases. Entrer le cas dans Votre test.

Le mode expression régulière a quelques différences avec le mode caractère, au delà (bien sûr) de l’utilisation des expressions régulières. La première est que ces fonctions sont appliquées aux parties de la chaîne qui correspondent à la chaîne de recherche, pas au champ entier. La seconde est que ces fonctions s’appliquent à la chaîne de remplacement, pas au champ entier.

La troisième et plus importante est que la chaîne de remplacement peut faire référence à des parties la chaîne de recherche en utilisant des références arrières. Une référence arrière is \\n où n est un entier qui se réfère aux n groupes mis entre parenthèses dans l’expression de recherche. Par exemple, prenons le même exemple que plus haut, a bad cat, une expression de recherche a (…) (…) et une expression de remplacement a \2 \1, le résultat sera a cat bad Merci de regardez le Tout à propos de l’utilisation des expressions régulières dans calibre pour plus d’informations sur les références arrières.

Un modèle utile : supposez que vous voulez changer la casse d’un champ entier. La manière la plus facile de faire ceci est d’employer le mode caractère, mais supposons que vous voulez aller plus loin en employant le mode expression régulière. L’expression de recherche devrait être (^.*$), l’expression de remplacement devrait être \1, et la fonction désirée sensible à la casse devra être sélectionnée.

Finalement, en mode expression régulière vous pouvez copier des valeurs d’un champ à un autre. Rendez simplement la source et le champ de destination différente. La copie peut remplacer le champ de destination, ajouter au champ (ajouté au début ), ou l’apposer au champ (ajoutez à l’extrémité). La case à cocher usecomma indique à calibre (ou pas) d’ajoutez une virgule entre le texte et le champ de destination dans les modes ajoutez au début et adjonction. Si la destination est multiple (par exemple, des étiquettes), alors vous ne pouvez pas décocher cette case.

Recherchez et remplacez est fait après que tous les autres changements de métadonnées dans les autres onglets sont appliqués. Ceci peut mener à une certaine confusion, parce que les cases de test montreront l’information avant que les autres changements, mais l’opération sera appliquée après les autres changements. Si vous avez n’importe quels doutes au sujet de ce qui va se produire, ne mélangez pas rechercher/remplacer avec d’autres changements.

Téléchargement de métadonnées par lot

Si vous voulez télécharger les métadonnées pour de multiples livres en une fois, faites un clic droit sur le bouton Editer les métadonnées et sélectionnez Télécharger les métadonnées et les couvertures. Vous pouvez choisir de télécharger seulement des métadonnées, seulement des couvertures, ou les deux.

Ajouter des fichiers de données supplémentaires à un livre
calibre peut stocker un nombre quelconque de fichiers de données supplémentaires associés à un livre. Il peut s’agir de couvertures alternatives, de matériel supplémentaire, etc. Ils ne peuvent pas être consultés directement ou utilisés comme sources de conversion. Ils ne sont pas non plus indexés par le moteur de recherche en texte intégral de calibre. Pour les voir, les ajouter ou les supprimer, sélectionnez le livre et faites un clic droit sur le bouton Editer les métadonnées et choisissez Gérer les fichiers de données. Cela ouvrira une fenêtre dans laquelle vous pourrez effectuer des opérations sur ces fichiers. Vous pouvez également faire un clic droit sur le bouton Ajouter des livres et choisir Ajouter des fichiers de données aux enregistrements des livres sélectionnés pour ajouter plus rapidement des fichiers de données.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Questions Fréquemment Posées

Questions Fréquemment Posées

Contenu

- Conversion de format de livre numérique

- Intégration d’un périphérique

- Gestion de la bibliothèque

- Divers

Conversion de format de livre numérique

Contenu

- Quels sont les formats que supporte calibre en entrée/sortie pour la conversion ?

- Quels sont les meilleurs formats source pour la conversion ?

- J’ai converti un fichier PDF, mais le résultat présente divers problèmes ?

- Comment puis-je convertir mon fichier contenant des caractères non-anglais, ou des guillemets intelligents ?

- Quel est le problème avec la Table des Matières dans les fichiers MOBI?

- Comment puis-je convertir une collection de fichiers HTML dans un ordre spécifique ?

- L’EPUB que j’ai produit avec calibre n’est pas valide ?

- Comment puis-je utiliser certaines les fonctionnalités avancées des outils de conversion ?

Quels sont les formats que supporte calibre en entrée/sortie pour la conversion ?

calibre supporte la conversion de nombreux formats en entrée vers de nombreux formats en sortie. Il peut convertir tous les formats d’entrée dans la liste ci-dessous, vers chaque format de sortie.

Formats d’entrée: 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

Formats de sortie : AZW3, EPUB, DOCX, FB2, HTMLZ, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

Note

PRC est un format générique, calibre supporte les fichiers PRC avec des entêtes TextRead et MOBIBook. PDB est aussi un format générique. calibre supporte les fichiers eReader, Plucker (format d’entrée uniquement), PML et zTxt PDB. Le support de DJVU est seulement pour la conversion de fichiers DJVU qui contiennent du texte intégré. Ceux-ci sont typiquement produits par un logiciel OCR. Les livres MOBI peuvent être de deux types Mobi6 et KF8. calibre supporte pleinement chacun des deux. Les fichiers MOBI ont souvent des extensions de fichier .azw ou .azw3. Les fichiers DOCX de Microsoft Word 2007 et ultérieurs sont supportés.

Quels sont les meilleurs formats source pour la conversion ?

Par ordre décroissant de préférence : LIT, MOBI, AZW, EPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

J’ai converti un fichier PDF, mais le résultat présente divers problèmes ?

PDF est un terrible format à partir duquel convertir. Pour une liste des différents problèmes que vous pouvez rencontrer lors de la conversion PDF, voir : Conversion de documents PDF.

Comment puis-je convertir mon fichier contenant des caractères non-anglais, ou des guillemets intelligents ?

Il y a deux aspects à ce problème :

1. Connaitre l’encodage du fichier source : calibre essaie de deviner quel encodage de caractères vos fichiers sources utilisent, mais souvent, c’est impossible, aussi devez-vous lui préciser l’encodage à utiliser. Cela peut être fait dans l’interface graphique utilisateur par l’intermédiaire du champ Encodage des caractères d’entrée dans la section Apparence → Texte de la boite de dialogue de conversion. Les outils en lignes de commande ont tous une option ebook-convert-txt-input --input-encoding.

2. Lors de l’ajout de fichiers HTML dans calibre, vous devrez peut-être indiquer à calibre quel est l’encodage des fichiers. Pour ce faire, allez dans Préférences → Avancé →  Extensions → extensions → Type de fichier, et personnalisez l’extension HTML to ZIP en lui indiquant quel encodage de caractère vous avez dans vos fichiers HTML Maintenant lorsque vous ajoutez des fichiers HTML dans calibre, ils seront traités correctement. Les fichiers HTML provenant de différentes sources ont souvent des encodages différents, de sorte que vous pouvez avoir à modifier ce paramètre à plusieurs reprises. Un codage courant pour beaucoup de fichiers sur le Web est cp1252 et je vous suggère de commencer par celui-là. Notez que, lors de la conversion des fichiers HTML, laissez le paramètre d’encodage d’entrée mentionnée ci-dessus à blanc. Car l’extension HTML to ZIP convertit automatiquement les fichiers HTML selon une norme d’encodage (utf-8).

Quel est le problème avec la Table des Matières dans les fichiers MOBI?

La première chose à comprendre est que la plupart des livres numériques ont deux tables des matières. L’une est la Table des Matières traditionnelle, comme la TdM que vous trouvez dans les livres papier. Cette Table des Matières est une partie du flux du document principal et peut être mise en page, comme bon vous semble. Cette TdM est appelée TdM de contenu.

Ensuite, il y a la TdM des métadonnées. Cette TdM ne fait pas partie du texte du livre et elle est généralement accessible par un bouton spécial sur une liseuse. Par exemple, dans la visionneuse de livre numérique de calibre, vous utilisez le bouton Afficher la Table des Matières pour y accéder. Cette TdM ne peut être mise en forme par le créateur du livre. Sa représentation dépend du logiciel de visualisation.

Dans le format MOBI, la situation est un peu confuse. C’est parce que le format MOBI, le seul parmi les formats de livre numérique principaux, ne gère pas correctement les TdM de métadonnées. Un livre MOBI simule la présence d’une TdM de métadonnées en ajoutant une TdM de contenu supplémentaire à la fin du livre. Lorsque vous cliquez sur Aller à la Table des Matières sur votre Kindle, c’est vers cette TdM de contenu supplémentaire que le Kindle vous emmène.

Maintenant, il pourrait bien vous sembler que le livre MOBI a deux TdM identiques. Rappelez-vous que, sémantiquement parlant, il y a une TdM de contenu et une TdM de métadonnées, même si elles ont le même contenu et le même aspect. L’une est accessible directement depuis le menu Kindle, l’autre ne peut pas l’être.

Lors de la conversion vers MOBI, calibre détecte la TdM de métadonnées dans le document d’entrée et génère une TdM de fin de fichier dans le fichier MOBI de sortie. Vous pouvez désactiver cette fonction par une option dans les paramètres de sortie MOBI. Vous pouvez aussi dire à calibre de la mettre au début ou la fin de l’ouvrage via une option dans les paramètres de sortie MOBI. Rappelez-vous que cette TdM est sémantiquement une TdM de métadonnées, dans n’importe quel autre format que MOBI elle ne peut pas faire partie du texte. Le fait qu’il s’agisse d’une partie du texte dans MOBI est un accident causé par les limitations de MOBI. Si vous voulez une TdM à un endroit particulier dans le document texte, créez-la à la main. Nous vous recommandons fortement de laisser la valeur par défaut, c’est à dire avec la TdM de métadonnées à la fin du livre. Notez également que si vous désactivez la génération d’une TdM de fin de fichier, le fichier MOBI résultant pourrait ne pas fonctionner sur Kindle, vu que Kindle utilise la TdM de métadonnées pour beaucoup de choses, incluant la fonctionnalité Tourner la Page.

Si vous avez une TdM créée à la main dans le document d’entrée, vous pouvez utiliser les options de détection de TdM dans calibre pour générer automatiquement une TdM de métadonnées à partir de celle-ci. Voir la section sur la conversion du Manuel de l’utilisateur pour plus de détails sur la façon d’utiliser ces options.

Enfin, je vous encourage à abandonner la création d’une TdM de contenu et de n’avoir qu’une TdM de métadonnées dans vos lvres numériques. La TdM de métadonnées fournira aux personnes qui lisent vos livres numériques une expérience de navigation très supérieure (sauf sur le Kindle, où elle est essentiellement la même qu’une TdM de contenu).

Note

Le nouveau format AZW3 a sa propre prise en charge d’une TdM de métadonnées. Cependant, le micrologiciel de la Kindle a tendance à mal fonctionner si vous désactivez la génération de la TdM interne de fin de fichier. Aussi il est recommandé de laisser la TdM générée seule. Si vous créez un fichier AZW3 avec une TdM de métadonnées et pas de TdM générée de fin de fichier, certaines fonctionnalités sur la Kindle ne fonctionneront pas, telle que la fonctionnalité Tourner la Page.

Comment puis-je convertir une collection de fichiers HTML dans un ordre spécifique ?

Pour convertir une collection de fichiers HTML dans un ordre spécifique, vous devez créer un fichier table des matières. Celui-ci est, un autre fichier HTML qui contient des liens vers tous les autres fichiers dans l’ordre souhaité. Un tel fichier ressemble à

<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>

Ensuite, il suffit d’ajouter ce fichier HTML au GUI et d’utiliser le bouton Convertir pour créer votre livre numérique. Vous pouvez utiliser l’option dans la section Table des Matières de la boîte de dialogue Convertir pour contrôler la façon dont la Table des Matières est générée.

Note

Par défaut, lorsque vous ajoutez des fichiers HTML, calibre suit les liens dans les fichiers en adoptant un parcours en profondeur depth first order. Cela signifie que si A.html pointe vers B.html, C.html et D.html, mais que B.html a également un lien vers D.html, les fichiers seront dans l’ordre suivant : A.html, B.html, D.html, C.html. Si vous voulez l’ordre A.html, B.html, C.html, D.html alors vous devez dire à calibre d’ajouter vos fichiers dans un parcours en largeur breadth first order. Pour ce faire, allez dans Préférences → Avancé →  Extensions et personnalisez le module HTML to ZIP.

L’EPUB que j’ai produit avec calibre n’est pas valide ?

calibre ne garantit pas qu’un EPUB qu’il produit soit valide. La seule garantie qu’il donne est que si vous lui donnez du XHTML 1.1 + CSS 2.1 valide, il va vous afficher un EPUB valide. calibre est conçu pour les consommateurs de livres numériques, non pour les producteurs. Il s’efforce de s’assurer que les EPUBs qu’il produit fonctionnent réellement comme prévu sur une grande variété de périphériques, objectif qui est incompatible avec la production d’EPUBs valides, et qui est beaucoup plus important pour la grande majorité de ses utilisateurs. Si vous avez besoin d’un outil qui produit des EPUBs toujours valides, calibre n’est pas pour vous. Cela signifie, que si vous voulez envoyer un EPUB produit par calibre à une boutique en ligne qui utilise un vérificateur de validité d’EPUB, vous avez à assurer que l’EPUB est valide par vous même, calibre ne le fait pas pour vous – en d’autres mots vous devez alimenter calibre en XHTML + CSS valides comme documents d’entrée.

Comment puis-je utiliser certaines les fonctionnalités avancées des outils de conversion ?

Vous pouvez obtenir de l’aide sur n’importe quelle caractéristique particulière des convertisseurs en passant la souris au-dessus de l’interface graphique ou en exécutant ebook-convert dummy.html .epub -h dans un terminal. Un bon endroit pour démarrer est de regarder le fichier de démonstration suivant qui illustre quelques-unes des fonctionnalités avancées html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

Intégration d’un périphérique

Contenu

- Quels périphériques calibre supporte-t’il ?

- Comment puis-je aider à ce que mon périphérique soit pris en charge dans calibre ?

- Mon périphérique n’est pas détecté par calibre ?

- Mon périphérique est non standard ou inhabituel. Comment faire pour le connecter ?

- Comment utiliser calibre avec mon iPad/iPhone/iPod touch ?

- Comment utiliser calibre avec mon téléphone/ma tablette Android ou ma Kindle Fire ?

- Puis-je accéder à mes livres calibre en utilisant le navigateur web sur mon Kindle ou sur un autre périphérique de lecture ?

- Je ne peux pas envoyer des courriels en utilisant calibre ?

- Mon appareil est monté en lecture seule dans Linux, donc calibre ne peut pas s’y connecter ?

- Pourquoi calibre ne prend pas en charge les collections sur Kindle ou les étagères sur Nook ?

- J’obtiens un message d’erreur lorsque j’essaie d’utiliser calibre avec mon Kobo Touch/Glo/etc.?

- Les couvertures des livres que j’envoie à ma Kindle apparaissent momentanément et sont ensuite remplacées par une couverture générique ?

- Les couvertures de mes fichiers MOBI n’apparaissent plus sur mon Kindle pour PC/Kindle pour Android/iPad etc.

- J’ai transféré des livres vers ma Kindle en utilisant calibre et ils n’y apparaissent pas ?

Quels périphériques calibre supporte-t’il ?

calibre peut se connecter directement aux principaux (et la plupart des moins connus) périphériques de lecture de livre numérique, smartphones, tablettes, etc. En outre, en utilisant la fonction Connecter au dossier, vous pouvez l’utiliser avec n’importe quel lecteur de livre numérique qui se comporte comme un disque USB. Pour finir, vous pouvez vous connecter sans fil à tout périphérique qui a un navigateur web en utilisant le Serveur de contenu calibre.

Comment puis-je aider à ce que mon périphérique soit pris en charge dans calibre ?

Si votre périphérique apparaît comme un disque USB dans le système d’exploitation, ajouter la prise en charge de celui-ci à calibre est très facile. Nous avons juste besoin de connaître quelques informations de votre part :

- La liste complète des formats de livre numérique que votre périphérique prend en charge.

- Il y a-t’il un dossier spécial dans lequel tous les fichiers livre numérique devront être placés ? Est-ce que le périphérique détecte les fichiers placés dans les sous-dossiers ?

- Nous avons également besoin d’informations sur votre périphérique que calibre recueillera automatiquement. Tout d’abord, si votre périphérique prend en charge les cartes SD, insérez-en une. Ensuite, connectez votre périphérique à l’ordinateur. Dans calibre allez dans Préférences → Avancé → Divers et cliquez sur le bouton « Déboguez la détection du périphérique ». Cela va créer un journal de débogage. Copiez-le dans un fichier et répétez le processus, mais cette fois avec votre périphérique déconnecté de votre ordinateur.

- Envoyez-nous les deux journaux de débogage ainsi que les autres informations recueillies et nous écrirons un pilote pour votre périphérique.

Une fois que vous nous envoyez les informations sur un système d’exploitation particulier, la prise en charge pour ce périphérique dans ce système d’exploitation apparaîtra dans la prochaine version de calibre. Pour nous envoyer les fichiers, ouvrez un rapport de bug et attachez vos informations en pièces jointes. Voir comment rapporter des bugs [https://calibre-ebook.com/fr/bugs].

Mon périphérique n’est pas détecté par calibre ?

Suivez ces étapes pour trouver le problème :

- Assurez-vous que vous connectez un seul périphérique à la fois à votre ordinateur. Ne pas avoir un autre périphérique pris en charge par calibre comme un iPhone/iPad etc. en même temps.

- Si vous connectez un iDevice Apple (iPad, iPod Touch, iPhone), Apple ne permet plus à des applications tierces de se connecter à leurs périphériques en utilisant un câble USB. A la place veuillez utiliser une connexion sans fil via le Serveur de contenu calibre.

- Si vous connectez un Kindle Fire ou un autre appareil Android, lisez la note sous En utilisant un cable USB.

- Sous macOS si vous obtenez des erreurs d’autorisation lors de la connexion d’un périphérique à calibre, vous pouvez corriger cela en regardant sous :guilabel:`Préférences système > Sécurité et confidentialité > Confidentialité > Fichiers et répertoires”.

- Assurez-vous que vous utilisez la dernière version de calibre (Actuellement 7.10.0. La dernière version peut être téléchargée à partir du site web de calibre [https://calibre-ebook.com/fr/download]. Vous pouvez voir quelle version de calibre vous utilisez actuellement en regardant la ligne du bas de la fenêtre principale de calibre.

- Vérifiez que votre système d’exploitation peut voir le périphérique. Autrement dit, le périphérique devrait apparaître dans l’Explorateur Windows (sous Windows) ou le Finder (sous macOS).

- Dans calibre, allez dans Préférences → Périphérique à ignorer et vérifiez que votre périphérique n’est pas ignoré.

- Si toutes les étapes précédentes ont échoué, allez dans Préférences → Divers et cliquez sur Déboguer la détection de périphérique avec votre périphérique connecté et postez la sortie comme un ticket sur le traqueur de bug calibre [https://bugs.launchpad.net/calibre].

Mon périphérique est non standard ou inhabituel. Comment faire pour le connecter ?

En plus de la fonction Se connecter au dossier qui se trouve dans le menu contextuel du bouton Connecter/partager, calibre fournit une extension de périphérique Définie par l'utilisateur qui peut être utilisée pour se connecter à n’importe quel périphérique USB considéré comme un lecteur de disque par votre système d’exploitation. Remarque : Sous Windows, le périphérique doit avoir une lettre de lecteur pour pouvoir être utilisé par calibre. Voir l’extension de périphérique Préférences -> Extensions -> Extensions interface du périphérique -> User defined et Préférences -> Divers-> Récupérer les informations pour définir les préférences de l'utilisateur pour les périphériques connectés pour plus d’informations. Notez que si vous utilisez un module d’extension personnalisé sur un périphérique normalement détecté par une extension de périphérique calibre, vous devez désactivez tout d’abord l’extension intégrée, de ce fait l’extension de périphérique définie par l’utilisateur est utilisée à la place.

Comment utiliser calibre avec mon iPad/iPhone/iPod touch ?

Une autre manière aisée de naviguer dans votre collection calibre sur votre périphérique Apple est d’utiliser le le Serveur, qui rend votre collection disponible sur le net. Effectuez d’abord les étapes suivantes dans calibre

- Définir le Format de Sortie Préféré de calibre à ePub (le format de sortie peut être paramétré dans Préférences → Interface → Comportement)

- Définissez le profil de sortie pour iPad (cela fonctionnera pour iPhone/iPod aussi), sous Préférences → Conversion → Options communes → Mise en page

- Convertir les livres que vous voulez lire sur votre iDevice au format EPUB en les sélectionnant et en cliquant sur le bouton Convertir.

- Lancez le Serveur de contenu en cliquant sur le bouton Connecter/partager et laissez calibre ouvert. Vous pouvez aussi dire à calibre de démarrer automatiquement le serveur de contenu via Préférences → Partager → Partager via internet.

Le Serveur de contenu calibre vous permet de lire des livres directement dans Safari. En complément, il y a beaucoup d’autres applications pour votre iDevice qui peuvent se connecter au Serveur de contenu calibre. Par exemple : Marvin, Mapleread et iBooks lui-même.

Utilisation du serveur de contenu

Lancez le navigateur Safari et saisissez l’adresse IP et le port de l’ordinateur exécutant le serveur calibre, comme ceci :

http://192.168.1.2:8080/

Remplacez 192.168.1.2 par l’adresse IP locale de l’ordinateur qui fait tourner le serveur calibre. Voir Le Serveur de contenu calibre pour plus d’informations sur l’exécution du serveur et comment trouver la bonne adresse IP à utiliser.

Vous verrez une liste de livres dans Safari, cliquez sur n’importe quel livre et il vous sera donné l’opportunité de soit le télécharger soit de le lire dans le navigateur. Si vous choisissez de le télécharger, Safari vous invite ensuite à l’ouvrir avec iBooks.

De nombreuses applications de lecture permettent de parcourir la bibliothèque calibre directement via son support OPDS. Dans ces apps, vous pouvez accéder à l’écran du catalogue en ligne et ajouter l’adresse IP du serveur calibre pour parcourir et télécharger les livres de votre bibliothèque calibre dans l’app.

Comment utiliser calibre avec mon téléphone/ma tablette Android ou ma Kindle Fire ?

Il existe deux solutions pour connecter votre périphérique Android à calibre. En utilisant un câble USB – ou sans fil, grâce au wifi. La première étape pour un périphérique Android est d’installer une application de lecture de livre numérique. Il existe de nombreuses applications de lecture de livre numérique gratuites ou payantes pour Android : quelques exemples (sans ordre particulier) : 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].

En utilisant un cable USB

Il suffit de brancher votre périphérique à l’ordinateur avec un câble USB. calibre devrait détecter automatiquement le périphérique et alors vous pourrez transférer des livres en cliquant sur le bouton guilabel:Envoyer au périphérique. Notez que sous MacOS et Linux, un seul programme peut se connecter à un appareil Android à la fois. Assurez-vous donc que l’appareil n’est pas ouvert dans le gestionnaire de fichiers de l’OS, ou dans l’utilitaire de transfert de fichiers Android, etc.

Note

Avec les nouveaux appareils Android, vous pourriez avoir à faire quelques pirouettes pour réussir à faire fonctionner la connexion, puisque Google ne veut vraiment pas que vous soyez indépendant de son nuage. Tout d’abord, déverrouillez l’écran avant de brancher le câble USB. Lorsque vous branchez le câble USB, vous recevrez un popup de notification. Assurez-vous qu’il indique quelque chose comme «&nbsp;Transfert de fichiers multimédias&nbsp;» ou «&nbsp;MTP (mode de transfert de média)&nbsp;». Si ce n’est pas le cas, tapez sur la notification et changez le mode en transfert multimédia (MTP). Vous devrez peut-être redémarrer calibre à ce stade pour que votre périphérique soit reconnu. Enfin, vous pouvez obtenir un pop-up sur l’appareil à chaque fois que calibre ou le système d’exploitation essaie réellement de s’y connecter, en demandant la permission, appuyez sur OK.

Note

Avec le Kindle Fire 8 ou plus récent, une icône s’affiche lorsque le câble USB est branché, indiquant que l’appareil est en cours de chargement. Touchez cette icône et mettez l’appareil en mode de transfert de données, puis lancez calibre, il devrait alors être détecté.

Sans fil
calibre possède un serveur web intégré, le Le Serveur de contenu calibre. Il rend votre collection calibre disponible sur le net. Vous pouvez la consulter sur votre appareil en utilisant un simple navigateur ou une application dédiée. Effectuez d’abord les étapes suivantes dans calibre

- Paramétrez le Format Préféré de Sortie de calibre à EPUB pour les périphériques Android, ou MOBI pour Kindle (Le format de sortie peut être réglé dans Préférences → Interface → Comportement)

- Convertir les livres que vous voulez lire sur votre périphérique au format EPUB en les sélectionnant et en cliquant sur le bouton Convertir.

- Activez le Serveur de contenu dans les préférences de calibre et laissez calibre en exécution.

Maintenant, sur votre périphérique Androïd, ouvrez le navigateur et naviguez vers

http://192.168.1.2:8080/

Remplacez 192.168.1.2 par l’adresse IP locale de l’ordinateur qui fait tourner le serveur calibre. Voir Le Serveur de contenu calibre pour plus d’informations sur l’exécution du serveur et comment trouver la bonne adresse IP à utiliser.

Vous pouvez maintenant naviguer dans votre bibliothèque de livres et télécharger les livres depuis calibre sur votre périphérique pour les ouvrir avec n’importe quel logiciel de lecture de livre numérique que vous avez préalablement installé sur votre périphérique Android.

De nombreuses applications de lecture permettent de parcourir la bibliothèque calibre directement via son support OPDS. Dans ces apps, vous pouvez accéder à l’écran du catalogue en ligne et ajouter l’adresse IP du serveur calibre pour parcourir et télécharger les livres de votre bibliothèque calibre dans l’app.

Puis-je accéder à mes livres calibre en utilisant le navigateur web sur mon Kindle ou sur un autre périphérique de lecture ?

Calibre dispose d’un serveur de contenu qui exporte les livres dans calibre en tant que page Web. Voir Le Serveur de contenu calibre pour plus de détails.

Certains périphériques, comme le Kindle (1/2/DX), ne permettent pas d’accéder au port 8080 (Le port par défaut sur lequel le serveur de contenu fonctionne). Dans ce cas, remplacer le port dans les Préférences de calibre par 80. (Sur certains systèmes d’exploitation, vous pouvez ne pas être en mesure de lancer le serveur sur un numéro de port inférieur à 1024 en raison des paramètres de sécurité. Dans ce cas, la solution la plus simple est de régler votre routeur pour rediriger les requêtes sur leport 80 vers le port 8080).

De plus, le navigateur de certains périphériques n’est pas assez perfectionné pour faire fonctionner l’interface utilisée par le Serveur de contenu. Pour de tels appareils, vous pouvez simplement ajouter /mobile à l’URL du serveur pour obtenir une interface simplifiée et non Javascript.

Je ne peux pas envoyer des courriels en utilisant calibre ?

En raison de la grande quantité de spams dans les courriers électroniques, l’envoi de courriels peut être délicat, car les différents serveurs de messagerie utilisent différentes stratégies pour bloquer les courriels. Le problème est particulièrement courant si vous envoyez le courriel directement (sans relais de messagerie) par calibre. Beaucoup de serveurs (par exemple, Amazon) bloquent les courriels qui ne proviennent pas d’un relais connu. Pour configurer l’envoi de courriels dans calibre faites ce qui suit :

- Créer un compte GMX gratuit sur GMX [https://www.gmx.com].

- Aller dans Préférences → Partager → Partager des livres par courriel, cliquez sur le bouton Utiliser GMX et remplissez les informations demandées.

- Identifiez-vous sur votre compte GMX sur le site web et activez l’envoi SMTP (`Paramètres Mail->Fonctions POP3 & IMAP-> Envoyer et recevoir des emails via des programmes externes)

- calibre sera alors capable d’utiliser GMX pour envoyer le mail

- Si vous envoyez à votre Kindle, n’oubliez pas de mettre à jour les préférences de messagerie sur votre page Amazon Kindle afin d’autoriser les courriers électroniques envoyés depuis votre adresse électronique GMX. Notez également qu’Amazon n’autorise pas l’envoi par e-mail des fichiers MOBI AZW3 et du nouveau style (KF8). Enfin, Amazon a récemment commencé à envoyer des e-mails de confirmation sur lesquels vous devez cliquer pour revenir à votre compte GMX avant que le livre ne soit effectivement livré.

Même après avoir fait cela, vous pouvez avoir des problèmes. Une source fréquente de problèmes est que certains programmes antivirus, mal conçus, bloquent calibre à l’ouverture de la connexion pour envoyer les courriels. Essayez d’ajouter une exclusion pour calibre dans votre programme antivirus.

Note

Microsoft/GMX peuvent désactiver votre compte si vous l’utilisez pour envoyer de grandes quantités de courriels. Aussi, lors de l’utilisation de ces services calibre se limite automatiquement à l’envoi d’un livre toutes les cinq minutes. Si vous n’avez pas peur de risquer que votre compte soit bloqué, vous pouvez réduire cet intervalle d’attente en allant dans Préférences → Avancé → Ajustements dans calibre.

Note

Récemment, Google a délibérément interrompu la prise en charge de son protocole d’envoi d’e-mails (SMTP) dans le but de forcer tout le monde à utiliser son interface Web afin de pouvoir vous montrer davantage de publicités. Ils essaient de prétendre que le SMTP n’est pas sûr, ce qui est faux et n’est qu’une excuse. Utilisez plutôt un autre fournisseur de messagerie.

Note

Si vous êtes inquiet de donner à calibre accès à votre compte courriel, créez simplement un nouveau compte courriel gratuit avec GMX ou Outlook et employez-le seulement pour calibre.

Mon appareil est monté en lecture seule dans Linux, donc calibre ne peut pas s’y connecter ?

Les noyaux Linux montent les périphériques en lecture seule lorsque qu’il y a des erreurs dans leur système de fichiers. Vous pouvez les réparer en faisant :

sudo fsck.vfat -y /dev/sdc

Remplacez /dev/sdc avec le chemin pointant vers le fichier représentant votre périphérique. Vous pouvez trouver quel est le nom de fichier représentant votre périphérique, qui sera toujours dans /dev en examinant le résultat de la commande :

mount

Pourquoi calibre ne prend pas en charge les collections sur Kindle ou les étagères sur Nook ?

Ni Kindle ni le Nook ne fournissent de manière de manipuler des collections sur une connexion USB. Si vous vous inquiétez vraiment d’employer des collections, je vous inviterais à vendre votre Kindle/Nook et à obtenir une Kobo. Seulement Kobo semble comprendre que la vie est trop courte pour écrire des collections une par une sur un écran e-ink 😇

Notez que, dans le cas du Kindle, il existe un moyen pour manipuler les collections via USB, mais il faut que le Kindle soit réinitialisé à chaque fois qu’il est déconnecté de l’ordinateur, pour que les changements apportés aux collections soient reconnus. En tant que tel, il est peu probable que les développeurs de calibre soient assez motivés pour faire les développements nécessaires. Il y a cependant une extension calibre qui vous permet de créer des collections sur votre Kindle à partir des métadonnées calibre. Elle est disponible ici [https://www.mobileread.com/forums/showthread.php?t=244202].

Note

Amazon a complètement supprimé la possibilité de manipuler les collections sur leurs modèles plus récents, comme le Kindle Touch et Kindle Fire, si bien que même l’extension ci-dessus serait inutile, à moins que vous ne rootez votre Kindle et installez un micrologiciel personnalisé.

J’obtiens un message d’erreur lorsque j’essaie d’utiliser calibre avec mon Kobo Touch/Glo/etc.?

La Kobo a un firmware très bugué. S’y connecter est connu pour échouer aléatoirement. Certaines combinaisons de carte mère, ports USB/câbles/hubs peuvent exacerber cette tendance. Si vous obtenez une erreur lors de la connexion de votre Touch à votre bibliothèque calibre essayez ce qui suit, chacune a résolu le problème pour certains utilisateurs de calibre.

- Connectez la Kobo directement à votre ordinateur, et non via une Hub USB

- Essayez avec un câble USB et un port USB différents sur votre ordinateur

- Se déconnecter de la Kobo et s’y connecter à nouveau, ceci entraîne la reconstruction de la base de données, solutionnant les erreurs de base de données corrompues.

- Essayez de mettre à jour le firmware de votre Kobo Touch avec la dernière version

- Essayez de réinitialiser la Kobo (parfois cela résout le problème pendant un petit moment, mais il réapparaît. Dans ce cas, vous devez réinitialiser à plusieurs reprises)

- Essayez seulement de mettre un ou deux livres à la fois sur la Kobo et de ne pas garder de grandes quantités de livres sur la Kobo

Les couvertures des livres que j’envoie à ma Kindle apparaissent momentanément et sont ensuite remplacées par une couverture générique ?

Cela se produit à cause d’un bug d’Amazon. Ils essaient de télécharger une couverture pour le livre à partir de leurs serveurs et lorsque cela échoue, ils remplacent la couverture existante créée par calibre par une couverture générique. Pour plus de détails, voir ce fil de discussion du forum [https://www.mobileread.com/forums/showthread.php?t=329945]. A partir de la version 4.17, calibre a une solution de contournement, où si vous connectez le Kindle à calibre après que les couvertures aient été détruites par Amazon, calibre les restaurera automatiquement. Donc pour voir les couvertures sur votre Kindle, vous devez :

1. Envoyez le livre à la Kindle avec calibre

2. Déconnecter la Kindle et attendre qu’Amazon détruise la couverture

3. Reconnecter la Kindle à calibre

Notez que ce contournement ne fonctionne que pour les livres envoyés avec calibre 4.17 ou ultérieur. Sinon, gardez simplement votre Kindle en mode avion, vous ne voulez pas vraiment qu’Amazon connaisse chaque livre que vous lisez de toute façon. Je vous encourage à contacter le service clientèle d’Amazon et à vous plaindre bruyamment de ce bogue. Peut-être qu’Amazon vous écoutera.

Note

Si le contournement ne fonctionne pas pour vous, assurez-vous que votre micrologiciel Kindle est au moins de la version 5.12.5, sortie en avril 2020.

Les couvertures de mes fichiers MOBI n’apparaissent plus sur mon Kindle pour PC/Kindle pour Android/iPad etc.

Ceci est causé par un bug du logiciel Amazon. Vous pouvez contourner le problème en allant dans Préférences → Converion → Options de sortie → sortie MOBI et mettre à oui l’option Activer le partage du contenu du livre. Si vous reconvertissez un livre précédemment converti, vous devrez également activer cette option dans la boîte de dialogue de conversion pour ce livre individuel (vu que par livre les paramètres sont enregistrés et ont la priorité).

Notez que cela aura pour conséquence que le MOBI généré sera affiché dans les documents personnels à la place de Books sur le Kindle Fire et que l’Amazon whispersync ne fonctionnera pas. Mais les couvertures seront présentes. C’est à vous de voir quelle fonctionnalité est la plus importante pour vous. Je vous encourage à contacter Amazon et leur demander de corriger ce bug.

Le bogue dans le logiciel d’Amazon consiste à supposer que lorsque vous mettez un fichier MOBI dans un Kindle, à moins que celui-ci ne soit marqué comme Document Personnel, vous avez acheté celui-ci chez Amazon et essaye de télécharger une imagette de la couverture à partir de ses serveurs. Quand le téléchargement échoue, il refuse d’utiliser la couverture définie dans le fichier MOBI comme solution de rechange. Ceci est probablement un comportement délibéré de la part d’Amazon afin d’essayer de forcer les auteurs à vendre par leur intermédiaire. En d’autres termes, le Kindle affiche seulement les couvertures pour les livres marqués comme Documents Personnels ou les livres achetés directement chez Amazon.

Si vous envoyez un fichier MOBI à une Kindle e-ink avec Calibre en utilisant une connexion USB, calibre pallie à ce bogue d’Amazon en téléchargeant de lui-même une vignette de la couverture. Cependant, cette solution n’est possible que lorsque une connexion USB est utilisée et le fichier envoyé avec calibre. Veuillez noter que si vous envoyez le fichier MOBI en utilisant un email, Amazon marquera automatiquement celui-ci comme Document Personnel et la couverture fonctionnera, mais le livre apparaîtra dans les Documents Personnels.

J’ai transféré des livres vers ma Kindle en utilisant calibre et ils n’y apparaissent pas ?

Les livres envoyés à la Kindle n’apparaissent qu’une fois qu’ils ont été indexés par celle-ci. Cela peut prendre un peu de temps. Si le livre n’apparait pas après un certain temps, alors il est possible que l’indexeur de la Kindle ait crashé. Parfois, un livre particulier peut causer ce crash . Malheureusement, Amazon n’a pas encore fourni de moyen de déduire quel livre causait le crash sur la Kindle. Vos seuls recours sont de soit remettre votre Kindle aux paramètres d’usine, soit de vider tout les fichiers de sa mémoire en utilisant Windows Explorer (ou n’importe quel gestionnaire de fichier que vous utilisez) et ensuite envoyer les livres à nouveaux, un par un, jusqu’à ce que vous trouviez le livre problématique. Une fois trouvé, supprimez le de la Kindle et faites une conversion MOBI vers MOBi ou MOBI vers AZW3 dans calibre et renvoyez le. Ceci résoudra probablement le problème.

Gestion de la bibliothèque

Contenu

- Où sont stockés les fichiers des livres ?

- Comment calibre gère-t-il le nom des auteurs et les tris ?

- Pourquoi Calibre ne me laisse-t-il pas stocker mes livres dans ma propre structure de dossiers ?

- Pourquoi calibre n’a-t-il pas telle ou telle colonne ?

- Puis-je avoir une colonne indiquant le format ou le numéro ISBN ?

- Comment puis-je déplacer mes données calibre d’un ordinateur à un autre ?

- La liste des livres dans calibre est vide !

- J’obtiens des erreurs avec ma bibliothèque calibre sur un disque réseau/NAS ?

Où sont stockés les fichiers des livres ?

Lorsque vous lancez calibre pour la première fois, il va vous être demandé de préciser un dossier dans lequel stocker vos livres. Chaque fois que vous ajouterez un livre à calibre, il va copier le livre dans ce répertoire. Les livres seront disposés dans des sous-répertories par auteur et titre dans ce répertoire général. Notez que le contenu de ce dossier est automatiquement géré par calibre. Ne pas ajouter de fichiers/répertoires manuellement dans ce répertoire, car ils pourraient être supprimés automatiquement. Si vous souhaitez ajouter un fichier associé à un livre précis, utilisez la flèche en haut à droite de la boîte de dialogue Modifier les métadonnées. calibre mettra automatiquement ce fichier dans le dossier et le déplacera lorsqu’il y aura des changements sur le titre ou l’auteur.

Les métadonnées des livres sont stockées dans le fichier metadata.db au plus haut niveau du répertoire de la bibliothèque. Ce fichier est une base de données SQLite. Lorsque vous sauvegardez votre bibliothèque assurez-vous de copier l’intégralité du répertoire et tous ses sous-répertoires.

Le dossier de la bibliothèque et tout son contenu constituent ce qu’on appelle une bibliothèque calibre. Vous pouvez avoir plusieurs de ces bibliothèques. Pour gérer les bibliothèques, cliquez sur l’icône calibre sur la barre d’outils. Vous pouvez créer de nouvelles bibliothèques, supprimer/renommer les bibliothèques et basculer facilement entre les bibliothèques existantes.

Vous pouvez copier ou déplacer des livres entre les différentes bibliothèques (une fois que vous avez installé plus d’une bibliothèque) en faisant un clic-droit sur le livre et en sélectionnant l’action Copier dans la bibliothèque.

Comment calibre gère-t-il le nom des auteurs et les tris ?

Les noms d’auteur sont complexes, particulièrement au travers des cultures voir la note [https://www.w3.org/International/questions/qa-personal-names.fr] pour certaines de ces complexités. calibre a une stratégie très flexible pour gérer les noms d’auteur. La première chose à comprendre est que livres et auteurs sont des entités différentes dans calibre. Un livre peut avoir plus d’un auteur, et un auteur peut peut avoir plus d’un livre. Vous pouvez gérer les auteurs d’un livre par la boite de dialogue d’édition des métadonnées. Vous pouvez gérer les auteurs individuellement en faisant un clic-droit sur un auteur dans le Navigateur d’étiquettes sur la droite de la fenêtre principale de calibre et en sélectionnant Gérer les auteurs. En utilisant cette boîte de dialogue vous pouvez changer le nom d’un auteur et également comment ce nom est trié. Ceci changera automatiquement le nom d’un auteur dans tous les livres de cet auteur. Quand un livre a de multiples auteurs, séparer leurs noms en utilisant le caractère &.

Maintenant passons au tri par nom d’auteur :

- Quand un nouvel auteur est ajouté à calibre (ce qui arrive quand un livre écrit par un nouvel auteur est ajouté), calibre calcule automatiquement une chaîne de tri pour le livre et pour l’auteur.

- Les auteurs dans le Navigateur d’étiquettes sont triés selon la valeur de tri pour les auteurs. Rappelez-vous que ceci est différent du champ Tri par Auteur d’un livre.

- Par défaut, cet algorithme de tri suppose que le nom de l’auteur est au format Prénom Nom et génère un valeur de tri Nom, Prénom.

- Vous pouvez modifier cet algorithme en allant dans Préférences → Avancé → Ajustements et en paramétrant l’ajustement author_sort_copy_method.

- Vous pouvez forcer calibre à recalculer les valeurs de tri d’auteur pour chaque auteur en cliquant droit sur n’importe quel auteur et en sélectionnant Gérer Auteurs, puis en cliquant sur le bouton Recalculer toutes les valeurs de tri par auteur. Faites cette action, après avoir défini l’ajustement author_sort_copy_method à ce que vous souhaitez .

- Vous pouvez forcer calibre à recalculer les valeurs de tri d’auteur pour tous les livres en utilisant la boîte de dialogue Modifier les métadonnées par lot (sélectionner tous les livres et cliquez sur Modifier les métadonnées. Ensuite, cochez la case Effectuer un tri automatique par auteur, puis cliquez sur OK.)

- Quand les valeurs de tri de l’auteur sont recalculées pour les livres , calibre utilise les valeurs de tri pour chaque auteur individuel. Par conséquent, assurez-vous que les valeurs de tri individuelles par auteur sont correctes avant de recalculer les valeurs de tri de l’auteur pour les livres.

- Vous pouvez contrôler de quelle manière le Navigateur d’étiquettes va afficher les auteurs en utilisant leur nom ou en se basant sur leurs valeurs de tri en paramétrant l’ajustement categories_use_field_for_author_name dans Préférences → Avancé → Ajustements.

Notez que vous pouvez définir la valeur de tri d’un auteur individuellement de la manière que vous désirez en utilisant Gérer auteurs. Cela peut être utile lorsque calibre ne le fait pas correctement,sur les noms composés complexes comme Miguel de Cervantes Saavedra ou lorsqu’il s’agit de noms asiatiques comme Sun Tzu.

Avec toute cette flexibilité, il est possible de voir calibre gérer vos noms d’auteurs exactement comme vous le souhaitez. Prenons comme exemple, une demande habituelle : que calibre trie les auteurs par Nom, Prénom. Pour ce faire, et si la note qui se trouve ci-dessous ne s’applique pas à vous, alors :

- Paramétrez l’ajustement author_sort_copy_method à copy comme décrit ci-dessus.

- Redémarrez calibre. Ne pas modifier les métadonnées de livre avant de faire les étapes restantes.

- Changer tous les noms d’auteurs à Nom, Prénom en utilisant la boîte de dialogue Gérer auteurs.

- Après avoir changé tous les auteurs, cliquez sur le bouton Recalculer toutes les valeurs de tri par auteur.

- Appuyez sur OK, à ce moment calibre va modifier les auteurs dans tous vos livres. Cela peut prendre un certain temps.

Note

Lors du changement depuis Prénom Nom à Nom, Prénom, il est souvent le cas que les valeurs author_sort sont déjà au format Nom, Prénom. Si c’est le cas, procédez comme suit :

- Paramétrez l’ajustement author_sort_copy_method à copy comme décrit ci-dessus.

- Redémarrez calibre. Ne pas modifier les métadonnées de livre avant de faire les étapes restantes.

- Ouvrir la boîte de dialogue Gérer Auteurs. Cliquer sur le bouton Copier toutes les valeurs de tri d'auteur dans auteur.

- Vérifiez les auteurs pour être sûr du résultat. Vous pouvez toujours cliquer sur Annuler pour abandonner les modifications. Une fois que vous aurez cliqué sur OK, il n’y aura plus d’annulation possible.

- Appuyez sur OK, à ce moment calibre va modifier les auteurs dans tous vos livres. Cela peut prendre un certain temps.

Pourquoi Calibre ne me laisse-t-il pas stocker mes livres dans ma propre structure de dossiers ?

Le point essentiel de la gestion de la bibliothèque calibre, c’est qu’il fournit une interface de recherche et de tri pour localiser les livres beaucoup plus efficace que n’importe lequel des système de répertoire que vous pourriez concevoir pour votre collection. En effet, une fois que vous serez à l’aise avec l’interface de calibre pour trouver, trier et parcourir votre bibliothèque, vous ne ressentirez plus le besoin de faire la chasse à vos livres sur votre disque dur pour les retrouver. En gérant les livres selon sa propre structure de dossier Auteur -> Titre -> Fichiers du livre, calibre est capable d’atteindre un haut niveau de fiabilité et de standardisation. Pour illustrer pourquoi une interface de recherche/étiquetage est supérieure à la gestion des dossiers, considérez ce qui suit. Supposons que votre collection de livre est bien classée dans des dossiers avec le schéma suivant :

Genre -> Author -> Series -> ReadStatus

Dans ce schéma, il est très facile de trouver, par exemple, tous les livres de science-fiction écrits par Isaac Asimov dans la série Fondation. Mais si vous voulez trouver tous les livres de science-fiction - non lus, il n’y a aucun moyen simple de le faire avec ce schéma de dossier. Il aurait fallu choisir un schéma qui ressemble à :

ReadStatus -> Genre -> Author -> Series

Dans calibre, vous pouvez utiliser des étiquettes pour indiquer le genre et le statut de lecture et ensuite simplement utiliser une requête de recherche basique comme tag:scifi and not tag:read. calibre a même une interface graphique agréable : vous n’avez pas à apprendre son langage de recherche mais simplement à cliquer sur les étiquettes pour les inclure ou exclure de la recherche.

Pour ceux d’entre vous qui réclament avoir besoin d’accéder au système de fichiers pour récupérer vos livres à partir du réseau, calibre a un excellent Serveur de contenu qui vous donne accès à votre bibliothèque calibre depuis le net.

Si vous craignez que calibre cesse un jour d’être développé et laisse tous vos livres en rade, coincés dans sa structure de dossiers, explorez la puissante fonctionnalité : :quilabel:`Enregistrer sur le disque` dans calibre qui vous permet d’exporter tous vos fichiers dans une arborescence de dossiers d’une complexité quelconque fondée sur leurs métadonnées.

Enfin, la raison pour laquelle il y a des chiffres à la fin de chaque dossier de titre, est la robustesse. Ce nombre est le numéro d’identification de l’enregistrement du livre dans la base de données calibre. La présence du numéro vous permet d’avoir plusieurs enregistrements avec le même titre et noms d’auteur. C’est aussi une partie de ce qui permet à calibre de régénérer magiquement la base de données avec toutes les métadonnées si le fichier de base de données est corrompu. Étant donné que la mission de calibre est de vous amener à ne plus stocker les métadonnées dans les noms de fichiers et d’arrêter d’utiliser le système de fichiers pour trouver des choses, la robustesse accrue offerte par les numéros d’identification vaut bien des noms de dossiers plus laids.

Si vous n’êtes toujours pas convaincu, alors j’ai bien peur que calibre ne soit pas pour vous. cherchez ailleurs pour vos besoins catalogage de livres. Pour que tout soit clair, nous ne changerons pas notre méthode. Inutile de nous contacter pour tenter de nous faire changer d’avis.

Pourquoi calibre n’a-t-il pas telle ou telle colonne ?

calibre est conçu pour avoir des colonnes pour les champs les plus fréquemment et largement utilisés. En outre, vous pouvez ajouter toutes les colonnes que vous désirez. Les colonnes peuvent être ajoutées via Préférences → Interface → Ajout de colonnes personnalisées. Regardez le tutoriel UI Power tips [https://calibre-ebook.com/fr/demo#tutorials] pour apprendre à créer vos propres colonnes, ou lisez ce poste de blog [https://blog.calibre-ebook.com/calibre-custom-columns/].

Vous pouvez également créer des « colonnes virtuelles » qui contiennent des combinaisons de métadonnées à partir d’autres colonnes. Dans la boîte de dialogue ajouter une colonne utilisez les liens Création rapide pour créer facilement des colonnes pour montrer l’ISBN du livre ou les formats. Vous pouvez utiliser le puissant langage de modèle pour faire beaucoup plus avec les colonnes. Pour plus de détails, voir Le langage de modèle calibre.

Puis-je avoir une colonne indiquant le format ou le numéro ISBN ?

Oui, vous le pouvez. Suivez les instructions de la réponse ci-dessus pour ajouter des colonnes personnalisées.

Comment puis-je déplacer mes données calibre d’un ordinateur à un autre ?

Vous pouvez exporter toutes les données calibre (livres, paramètres et extensions) et les importer alors sur un autre ordinateur. Tout d’abord voyons comment exporter les données :

- Faites un clic droit sur l’icône calibre dans la barre d’outils principale et sélectionnez Exporter/Importer toutes les données calibre. Notez que si un périphérique est actuellement connecté, cette option du menu ne sera pas disponible - donc, déconnectez tout périphérique connecté. Cliquez alors sur le bouton étiqueté Exporter toutes vos données calibre. Vous verrez une liste de toutes vos bibliothèques calibre. Cliquez OK et choisissez un répertoire vide quelque part sur votre ordinateur. Les données exportées seront sauvegardées dans ce répertoire. Copiez simplement ce répertoire vers votre nouvel ordinateur et suivez les instructions ci-dessous pour importer les données.

- Installez calibre sur votre nouvel ordinateur et passez en revue l’Assistant de bienvenue, ce que vous faites là n’a pas d’importance, vu que vous importerez vos anciens paramètres à l’étape suivante. Vous aurez alors un calibre vide, avec uniquement le guide Getting Started dans votre bibliothèque. Encore une fois, faites un clic droit sur le bouton calibre et choisissez Exporter/Importer toutes les données calibre. Cliquez alors le bouton étiqueté Importer les données précédemment exportées. Sélectionnez le répertoire avec les données exportées que vous avez copiées plus tôt. Vous aurez alors une liste des bibliothèques que vous pouvez importer. Passer la liste en revue une par une, et sélectionnez le nouvel emplacement pour chaque bibliothèque (un emplacement est simplement un répertoire vide sur votre ordinateur). Cliquez OK. Après l’accomplissement de l’importation, calibre redémarrera, avec toutes vos anciennes bibliothèques, vos paramètres et vos extensions calibre.

Note

Cette fonctionnalité Importer/Exporter est seulement disponible à partir de la version 2.47 et ultérieures. Si vous avez une ancienne version de calibre, ou si vous rencontrez des problèmes avec l’importation/exportation vous pouvez simplement copier votre répertoire de bibliothèque calibre manuellement, comme décrit dans le paragraphe suivant.

Copier simplement le répertoire de la bibliothèque calibre depuis l’ancien vers le nouvel ordinateur. Vous pouvez trouver quel est le répertoire de la bibliothèque en cliquant sur l’icône calibre dans la barre d’outils et vous verrez le chemin vers la bibliothèque calibre actuelle en haut de la fenêtre.

Sur le nouvel ordinateur, démarrez calibre pour la première fois. Il va exécuter l’Assistant de bienvenue qui vous demandera de spécifier l’emplacement de la bibliothèque de calibre. Sélectionnez le dossier précédemment copié. Si l’ordinateur où vous avez transféré la bibliothèque a déjà une installation de calibre, l’Assistant de bienvenue ne se lancera pas. Dans ce cas, faites un clic droit sur l’icône calibre de la barre d’outils et sélectionnez le nouveau dossier copié. Vous aurez maintenant deux bibliothèques calibre sur votre ordinateur et vous pouvez basculer de l’une à l’autre en cliquant sur l’icône calibre dans la barre d’outils. Le transfert de votre bibliothèque de cette façon préservera toutes vos métadonnées, les étiquettes, les colonnes personnalisées, etc.

La liste des livres dans calibre est vide !

Afin de comprendre pourquoi cela s’est produit, il faut comprendre ce qu’est une bibliothèque calibre. Au niveau le plus élémentaire, une bibliothèque calibre est juste un répertoire. Chaque fois que vous ajoutez un livre à calibre, les fichiers de ce livre sont copiés dans ce répertoire (subdivisé en sous-répertoires par auteur et par titre). A la racine du répertoire contenant la bibliothèque calibre, vous verrez un fichier appelé metadata.db. Ce fichier stocke les métadonnées comme le titre/auteur/cote/étiquettes, etc pour tous les livres de votre bibliothèque calibre. La liste des livresque calibre affiche est crée par la lecture du contenu de ce fichier metadata.db.

Il peut y avoir deux raisons pour lesquelles calibre affiche une liste vide de livres :

- Votre dossier de bibliothèque calibre a changé d’emplacement. Cela peut arriver s’il était sur un disque externe et que la lettre de lecteur pour ce disque a changé. Ou si vous avez accidentellement déplacé le dossier. Dans ce cas, calibre ne peut plus trouver sa bibliothèque et démarre avec une bibliothèque vide à la place. Pour y remédier, faites un clic-droit sur l’icône calibre dans la barre d’outils et sélectionnez Changer/créer/déplacer une bibliothèque. Cliquez sur la petite icône bleue pour sélectionner le nouvel emplacement de votre bibliothèque calibre et cliquez sur OK. Si vous ne connaissez pas le nouvel emplacement rechercher le fichier metadata.db sur votre ordinateur.

- Votre fichier metadata.db a été supprimé/endommagé. Dans ce cas, vous pouvez demander à calibre de restaurer le fichier metadata.db à partir de ses sauvegardes. Faites un clic droit sur l’icône calibre dans la barre d’outils et sélectionnez Maintenance de la bibliothèque->Restaurer la base de données. Calibre va restaurer automatiquement metadata.db.

J’obtiens des erreurs avec ma bibliothèque calibre sur un disque réseau/NAS ?

Ne mettez pas votre bibliothèque calibre sur un disque réseau.

Un système de fichiers est une bête complexe. La plupart des systèmes de fichiers réseau ne comprennent pas les diverses caractéristiques du système de fichier utilisé par calibre. Certains ne supportent pas le verrouillage des fichiers, d’autres ne supportent pas le hardlinking. En outre, calibre est une application mono-utilisateur. Si vous exécutez accidentellement deux copies de calibre sur la même bibliothèque en réseau, des incidents peuvent se produire. Enfin, différents systèmes d’exploitation imposent des limitations sur le système de fichiers, donc si vous partagez votre lecteur réseau à travers plusieurs systèmes d’exploitation, une fois de plus, des incidents se produiront.

Pensez à utiliser le Serveur de contenu de calibre pour rendre les livres disponibles sur d’autres ordinateurs. Lancez calibre sur un seul ordinateur et accédez-y via le Serveur de contenu ou via une solution de Bureau à Distance.

Si vous devez partager la bibliothèque actuelle, utilisez un outil de synchronisation de fichiers comme Dropbox ou rsync plutôt qu’un disque réseau. Si vous utilisez un outil de synchronisation de fichiers, il est essentiel que vous vous assuriez que calibre et l’outil de synchronisation de fichiers ne cherchent pas à accéder à la bibliothèque en même temps. En d’autres termes, n’exécuter pas l’outil de synchronisation de fichiers et calibre au même moment.

Même avec ces outils il y a un risque de corruption/de perte de données. Ne le faire que si vous êtes prêt à vivre avec ce risque. En particulier, sachez que Google Drive est incompatible avec calibre : si vous mettez votre bibliothèque calibre dans Google Drive, vous subirez une perte de données. Voir ce poste [https://www.mobileread.com/forums/showthread.php?t=205581] pour des détails.

Divers

Contenu

- Amazon arrête la livraison par e-mail des fichiers MOBI ?

- Je veux que calibre télécharge les actualités de mon site préféré.

- Pourquoi le nom calibre ?

- Pourquoi calibre ne montre qu’une partie de mes polices sous macOS ?

- calibre ne démarre pas sous Windows ?

- calibre se bloque/plante occasionnellement ?

- Les outils Visionneuse de livres numériques et Édition de livres de calibre ne fonctionnent pas sous Windows ?

- Utiliser la visionneuse ou faire n’importe quelle conversion cause une erreur de permission refusée dans Windows

- Calibre crash/ ne démarre pas sous macOS ?

- Je n’obtiens qu’un écran noir ou blanc lorsque je lance la visionneuse de livres numériques calibre ?

- J’ai téléchargé le programme d’installation mais il ne fonctionne pas ?

- Mon antivirus affirme que calibre est un virus/cheval de troie ?

- Comment faire une sauvegarde de calibre ?

- Comment puis-je mettre des livres numériques EPUB achetés avec calibre (ou que dois-je faire avec les fichiers .acsm) ?

- J’ai une erreur “accès refusé” ?

- Puis-je avoir la métadonnée commentaire apparaître sur ma liseuse ?

- Comment obtenir que calibre utilise mon proxy HTTP ?

- Je voudrais certaines fonctionnalités ajoutées à calibre. Que puis-je faire ?

- Pourquoi calibre n’a pas de mise à jour automatique ?

- Quelle est la licence de calibre ?

- Comment exécuter calibre depuis ma clé USB ?

- Comment faire fonctionner des éléments de calibre comme le téléchargement d’actualités et le serveur de contenu sur mon propre serveur Linux ?

Amazon arrête la livraison par e-mail des fichiers MOBI ?

Amazon a annoncé [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/] qu’ils n’accepteront plus les fichiers MOBI envoyés par courriel aux adresses @kindle.com. Vous pouvez demander à calibre d’envoyer EPUB au lieu de MOBI en allant dans Préférences → Partage de livres par e-mail puis en supprimant MOBI de la liste des formats à envoyer à votre adresse e-mail @kindle.com et en ajoutant EPUB à la place.

Noter cependant que le système de prise en charge EPUB d’Amazon est très imparfait, et qu’il rejettera un certain nombre de fichiers EPUB qui fonctionnent partout ailleurs. Dans ce cas, vous pouvez essayer l’astuce suivante :

1. Convertir le fichier EPUB en MOBI

2. Convertir ensuite le fichier MOBI en EPUB et envoyer le fichier EPUB résultant

Cela supprimera tout le formatage avancé, les polices intégrées, etc., mais augmentera considérablement les chances qu’Amazon accepte l’EPUB.

Note

Si vous utilisiez auparavant la livraison par courriel des périodiques téléchargés par calibre, il est préférable de les envoyer par câble USB ou de les télécharger depuis le serveur de contenu de calibre via le navigateur intégré du Kindle. Toutefois, si vous souhaitez continuer à utiliser la livraison par courriel, vous pouvez essayer de changer le format de sortie dans Préférences->Comportement en EPUB, calibre téléchargera alors les nouvelles au format EPUB. Quant à savoir si Amazon acceptera ou non le format EPUB, c’est une toute autre question.

Je veux que calibre télécharge les actualités de mon site préféré.

Si vous êtes un minimum compétent en informatique, vous pouvez indiquer à calibre comment télécharger des actualités à partir de n’importe quel site de votre choix. Pour apprendre à le faire, voir Ajouter votre site web favori d’actualités.

Sinon, vous pouvez demander un site d’actualités en particulier en postant dans le calibre Recipes forum [https://www.mobileread.com/forums/forumdisplay.php?f=228].

Pourquoi le nom calibre ?

Faites votre choix :

- Converter And LIBRary for E-books

- Un produit de haut calibre

- Un hommage au Librie de SONY qui a été le premier lecteur de livre numérique e-ink

- Ma femme l’a choisi ;-)

Calibre se prononce cal-i-ber pas ca-li-bre. Si ça vous interroge, calibre est l’orthographe britannique / du Commonwealth pour caliber. En tant qu’Indien, c’était l’orthographe la plus naturelle pour moi.

Pourquoi calibre ne montre qu’une partie de mes polices sous macOS ?

calibre incorpore les polices dans les fichiers de livre numérique qu’il crée. Les fichiers de livre numérique gèrent uniquement les polices TrueType et Open Type (.ttf et .otf) incorporées. La plupart des polices sur les systèmes macOS sont au format .dfont, donc elles ne peuvent pas être incorporées. calibre affiche uniquement les polices TrueType et Open Type trouvées sur votre système. Vous pouvez obtenir beaucoup de telles polices sur le web. Il vous suffit de télécharger les fichiers .ttf/.otf et de les ajouter au dossier Library/Fonts dans votre dossier principal.

calibre ne démarre pas sous Windows ?

Il peut y avoir plusieurs raisons à cela :

- Si vous n’obtenez aucune erreur mais que la fenêtre de calibre n’apparaît pas, elle est probablement apparue hors de l’écran. Vous pouvez rassembler toutes les fenêtres sur l’écran actuel à l’aide de l’une des techniques décrites ci-dessous ici [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows].

- Si vous obtenez une erreur sur calibre indiquant que vous n’êtes pas en mesure d’ouvrir un fichier car ce fichier est utilisé par un autre programme, procédez comme suit :

- Désinstallez calibre

- Redémarrer votre ordinateur

- Réinstallez calibre. Mais ne démarrez pas Calibre à partir de l’assistant d’installation.

- Désactivez temporairement votre programme antivirus (déconnecter Internet au préalable, pour être sûr)

- Regardez dans le dossier défini comme étant votre bibliothèque calibre. Si vous voyez un fichier nommé metadata.db, supprimez-le.

- Démarrez calibre

- A partir de là, vous devriez être en mesure de démarrer calibre normalement.

- Si vous obtenez une erreur sur une fonction Python s’interrompant soudainement après la mise à jour de calibre, tout d’abord désinstallez calibre puis supprimez les répertoires C:Program FilesCalibre et C:Program FilesCalibre2 (s’ils existent). Ensuite, ré-installez et ça devrait fonctionner.

- Si vous obtenez une erreur dans l’Assistant de bienvenue à l’ouverture initiale de calibre, essayez de choisir un dossier comme :file;`C:bibliothèque` comme bibliothèque de calibre (calibre a parfois du mal avec les emplacements de bibliothèque si le chemin contient des caractères non-anglais, ou seulement des chiffres, etc).

- Essayez de l’exécuter en tant qu’administrateur (Cliquez avec le bouton droit de la souris sur l’icône et sélectionnez Exécuter en tant qu’administrateur)

Si Calibre ne démarre toujours pas, lancez une invite de commande (touche Windows et R, puis tapez cmd.exe dans la boîte de dialogue Exécuter qui apparaît). Dans l’invite de commande tapez la commande suivante et appuyez sur Entrée:

calibre-debug -g

Post any output you see in a help message on the Forum [https://www.mobileread.com/forums/forumdisplay.php?f=166].

calibre se bloque/plante occasionnellement ?

Il y a plusieurs choses possibles que je sais, qui peuvent causer cela :

- Vous avez récemment connecté un moniteur externe ou un téléviseur à votre ordinateur. Dans ce cas, chaque fois que calibre ouvre une nouvelle fenêtre comme la fenêtre Modifier les métadonnées ou la boîte de dialogue de Convertir, elle apparaît sur l’autre moniteur et vous ne le remarquez pas et vous pensez donc que calibre est bloqué. Déconnectez votre second moniteur et redémarrez calibre.

- Les programmes suivants ont été signalés comme causant des plantages dans calibre : Si vous exécutez l’un de ceux-là, fermez-le avant de démarrer calibre, ou désinstallez-le : RoboForm, Logitech SetPoint Settings, Constant Guard Protection by Xfinity, Spybot, Killer Network Manager, Nahimic UI Interface, Acronis True Image.

- Vous utilisez une souris USB/une tablette de la marque Wacom. Il existe une incompatibilité entre les pilotes Wacom et la boite à outils graphique utilisée par calibre. Essayez avec une souris d’une autre marque.

- Sur certaines versions 64 bit de Windows il existe des logiciels/paramètres de sécurité qui empêchent calibre 64 bit de fonctionner correctement. Si vous utilisez la version 64 bit de calibre essayez de passer à la version 32-bit.

- Si le plantage survient lorsque vous essayez de copier du texte à partir de la Visionneuse de livre numérique calibre, cela est du le plus souvent à une application surveillant/gérant votre presse papier que vous utilisez. Arrêtez-la et tout rentrera dans l’ordre.

- Si le plantage survient spécifiquement quand vous utilisez le dialogue de fichier, comme cliquer sur le bouton Ajouter des livres ou le bouton Enregistrer sur le disque, alors vous avez certains programmes qui ont installé des extensions Shell endommagées sur votre ordinateur. Les responsables connus comprennent : SpiderOak, odrive sync et Dell Backup and Recovery et Netdrive. Si vous avez l’un de ceux-là, désinstallez-les et tout ira bien. Vous pouvez également utiliser l’utilitaire NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html] pour voir quelles extensions Shell sont installées sur votre système et les désactiver individuellement, si vous ne voulez pas complètement désinstaller le programme. Rappelez-vous d’utiliser « Redémarrer Explorer » ou redémarrer votre ordinateur après avoir désactivé les extensions Shell.

Si aucun des points répertoriés ci-dessus ne s’appliquent à vous, alors il y a un autre programme sur votre ordinateur qui interfère avec calibre. Tout d’abord redémarrez votre ordinateur en mode sans échec, afin d’avoir aussi peu de programmes que possible en exécution, et voyez si le programme démarre. Si c’est le cas, alors vous savez que c’est un programme qui cause le problème. La cause la plus probable est un programme qui modifie le comportement d’autres programmes, tels qu’un antivirus, un pilote de périphérique, quelque chose comme RoboForm (une application de remplissage automatique de formulaires) ou une aide technique comme la Synthèse Vocale ou un Lecteur d’Ecran.

La seule façon de trouver le coupable est d’éliminer les programmes un par un et voir lequel est la cause du problème. Essentiellement, arrêtez un programme, exécutez calibre, vérifiez les plantages. S’ils se produisent encore, arrêtez un autre programme et ainsi de suite.

Les outils Visionneuse de livres numériques et Édition de livres de calibre ne fonctionnent pas sous Windows ?

Ces deux programmes utilisent l’accélération matérielle car ils intègrent une version du navigateur Chrome pour rendre le HTML. S’ils ne fonctionnent pas, ce sera en raison d’une incompatibilité avec les pilotes GPU (graphiques) de votre système. Essayez d’abord de les mettre à jour, puis redémarrez. Si cela ne résout pas le problème, vous pouvez définir la variable d’environnement « QTWEBENGINE_CHROMIUM_FLAGS » à la valeur « -disable-gpu » pour désactiver l’accélération matérielle. Voir cette page [https://doc.qt.io/qt-6/qtwebengine-debugging.html] pour plus de détails.

Utiliser la visionneuse ou faire n’importe quelle conversion cause une erreur de permission refusée dans Windows

Quelque chose sur votre ordinateur empêche calibre d’accéder à ses propres fichiers temporaires. Le plus souvent les permissions sur le répertoire Temp sont incorrectes. Allez au répertoire file:C:\Users\USERNAME\AppData\Local dans l’Explorateur de Windows et faites alors un clic droit sur le répertoire file:Temp, sélectionnez Propriétés et allez à l’onglet :guilabel;`Sécurité`. Assurez-vous que votre compte utilisateur à le contrôle complet sur ce répertoire.

Certains utilisateurs ont rapporté qu’exécuter la commande suivante dans une invite de commande Administrateur répare leurs permissions. Pour obtenir une Invite de Commande Administrateur, cherchez après cmd.exe dans le menu démarrer, faites alors un clic droit sur l’entrée de l’invite de commande et sélectionner Exécuter en tant qu’administrateur. Dans l’Invite de commande entrer la commande suivante et appuyer Enter:

icacls "%appdata%\..\Local\Temp" /reset /T

Alternativement, vous pouvez exécuter calibre en tant qu’Administrateur, mais faire cela entraînera que certaines fonctionnalités, telle que le glisser et déposer de, de ne pas fonctionner.

Finalement, certains utilisateurs ont rapporté que désactiver l’UAC résout le problème.

Calibre crash/ ne démarre pas sous macOS ?

Une cause fréquente de pannes sur macOS est l’utilisation de technologies d’accessibilité qui sont incompatibles avec la boite à outils graphique utilisée par calibre. Essayez de désactiver VoiceOver si vous l’avez. Puis allez sur Préférences Système->Système->Accès Universel et désactivez-le paramètre pour autoriser l’accès aux périphériques assistés dans tous les onglets. Une autre raison peut être une application tierce modifiant l’apparence du système, comme Smart Scroll

Vous pouvez obtenir des informations de débogage sur les incidents calibre qui ne démarre pas en lançant Console.app. Les informations de débogage y seront transcrites. Si ces informations contiennent une ligne ressemblant à :

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

alors le problème est probablement dû à un cache de police endommagé. Vous pouvez effacer le cache en suivant ces instructions [https://www.macworld.com/article/1139383/fontcacheclear.html]. Si ça ne le résout pas, cherchez un fichier de police corrompu sur votre système, dans ~/Library/Fonts ou quelque chose qui y ressemble. Un moyen facile de vérifier les polices corrompues dans macOS est de lancer l’application « Font Book », sélectionnez toutes les polices, puis dans le menu Fichier, choisissez « Valider les polices ».

Je n’obtiens qu’un écran noir ou blanc lorsque je lance la visionneuse de livres numériques calibre ?

Cela est dû à une incompatibilité entre Qt WebEngine, que la visionneuse utilise pour effectuer le rendu, et les pilotes de GPU de votre système. Essayez d’abord de mettre à niveau les pilotes de GPU. Si cela n’aide pas, vous pouvez essayer de désactiver l’accélération matérielle dans Qt WebEngine en définissant la variable d’environnement QTWEBENGINE_CHROMIUM_FLAGS à la valeur --disable-gpu. Voir Variables d’environnement pour savoir comment modifier les variables d’environnement.

J’ai téléchargé le programme d’installation mais il ne fonctionne pas ?

Le téléchargement depuis Internet peut parfois aboutir à un résultat corrompu. Si le programme d’installation de calibre que vous avez téléchargé ne s’ouvre pas, essayez de le télécharger à nouveau. Si le nouveau téléchargement ne fonctionne pas, téléchargez-le à partir d’un autre site <https://github.com/kovidgoyal/calibre/releases/latest>`_. Si le programme d’installation ne fonctionne toujours pas, alors quelque chose sur votre ordinateur empêche son exécution.

- Essayez de désactiver temporairement votre programme antivirus (Microsoft Security Essentials, ou Kaspersky ou McAfee ou Norton ou autre). C’est probablement le coupable si le processus de mise à jour est suspendu en cours de lancement.

- De même, si le programme d’installation ne fonctionne pas/revient en arrière et que vous avez Microsoft PowerToys en cours d’exécution, quittez-le.

- Essayez de redémarrer votre ordinateur et exécutez un nettoyeur de registre comme Wise registry cleaner [https://www.wisecleaner.com].

- Essayez une installation propre, C’est-à-dire, désinstallez calibre, supprimez C:\Program Files\Calibre2 (ou quelque soit l’endroit où vous avec choisi d’installer calibre précdemment) Réinstallez alors calibre. Notez que la désinstallation ne touche pas à vos livres ou à vos paramètres.

- Essayez de télécharger le programme d’installation avec un autre navigateur. Par exemple, si vous utilisez Microsoft Edge, essayez d’utiliser Firefox ou Chrome à la place.

- Si vous obtenez une erreur sur une DLL manquante sur Windows alors, très probablement, les autorisations sur le dossier temporaire sont incorrectes. Allez dans le dossier C:\Users\USERNAME\AppData\Local dans votre Explorateur Windows, puis faites un clic droit sur le dossier Temp, puis sélectionnez Propriétés et cliquez sur l’onglet Sécurité. Assurez-vous que votre compte utilisateur a le contrôle total sur ce dossier.

Si vous ne parvenez toujours pas à démarrer le programme d’installation et que vous êtes sous Windows, vous pouvez utiliser l’installateur de calibre portable [https://calibre-ebook.com/fr/download_portable], qui n’a pas besoin d’un programme d’installation (c’est juste un fichier ZIP).

Mon antivirus affirme que calibre est un virus/cheval de troie ?

La première chose à vérifier est que vous avez téléchargé calibre sur le site officiel [https://calibre-ebook.com/fr/download]. Assurez-vous que vous cliquez les liens de téléchargement sur la gauche, pas les publicités sur la droite. calibre est un programme très populaire et des gens peu scrupuleux peuvent offrir la possibilité de le télécharger sur des sites Web non recommandables.

Si vous avez le téléchargement officiel et que votre antivirus prétend toujours que calibre est un virus, alors, votre programme antivirus fait une erreur. Les programmes antivirus utilisent des technologies heuristiques, les modèles de code qui semblent suspicieux pour détecter les virus. C’est un peu comme le profilage racial. calibre est un produit entièrement open source. Vous pouvez réellement parcourir le code source vous-même (ou le faire faire par quelqu’un d’autre) pour vérifier que ce n’est pas un virus. Merci de signaler la faute d’identification à la companie où vous avez acheté votre logiciel antivirus. Si le programme antivirus vous empêche de télécharger/installer calibre, désactivez-le temporairement, installez calibre, puis réactivez-le.

Comment faire une sauvegarde de calibre ?

L’élément le plus important à sauvegarder est le dossier bibliothèque de calibre, qui contient tous vos livres et métadonnées. C’est le dossier que vous avez choisi pour votre bibliothèque calibre lors de l’exécution de calibre pour la première fois. Vous pouvez obtenir le chemin d’accès au dossier de la bibliothèque en cliquant sur l’icône calibre sur la barre d’outils principale. Vous devez sauvegarder ce dossier complet avec tous ses fichiers et sous-dossiers.

Vous pouvez basculer calibre pour utiliser une bibliothèque sauvegardée en cliquant simplement sur l’icône calibre sur la barre d’outils et en sélectionnant le répertoire de sauvegarde de votre bibliothèque. Un dossier de sauvegarde de la bibliothèque conserve vos colonnes personnalisées, les recherches enregistrées ainsi que tous vos livres et métadonnées.

Si vous souhaitez sauvegarder les extensions de configuration calibre, vous devez sauvegarder le dossier config. Vous pouvez trouver ce dossier config via Préférences → Divers. Notez que la restauration des dossiers de configuration n’est pas officiellement supportée, mais devrait fonctionner dans la plupart des cas. Il suffit de copier le contenu du dossier de sauvegarde dans le répertoire de configuration à restaurer.

Comment puis-je mettre des livres numériques EPUB achetés avec calibre (ou que dois-je faire avec les fichiers .acsm) ?

La plupart des livres EPUB achetés ont un DRM. Ceci empêche calibre de les ouvrir. Vous avez toujours la possibilité d’utiliser calibre pour stocker et transférer les livres sur votre liseuse. Tout d’abord, vous devez autoriser votre liseuse sur Windows avec le logiciel Adobe Digital Editions. Une fois cela fait, les livres EPUB transférés avec calibre fonctionneront sur votre liseuse. Lorsque vous achetez un livre EPUB sur un site Web, vous obtiendrez un fichier .acsm. Ce fichier doit être ouvert avec Adobe Digital Editions, qui téléchargera ensuite le livre sous format .ePub. Le fichier de livre numérique sera stocké dans le dossier « My Digital Editions », d’où vous pourrez l’ajouter à calibre.

J’ai une erreur “accès refusé” ?

Une erreur d’accès refusé peut se produire pour de nombreuses raisons possibles, aucune n’ayant un rapport avec calibre.

- Vous pouvez obtenir des erreurs accès refusé si vous utilisez une carte SD avec protection en écriture activée.

- Sous macOS si vous obtenez des erreurs d’autorisation lors de la connexion d’un périphérique à calibre, vous pouvez corriger cela en regardant sous :guilabel:`Préférences système > Sécurité et confidentialité > Confidentialité > Fichiers et répertoires”.

- Si vous, ou un certain programme que vous utilisez a changé les permissions en lecture seule sur les fichiers en question.

- Si une erreur de système de fichier sur votre périphérique a amené votre système d’exploitation à monter le système de fichier en lecture seule ou marqué un fichier particulier comme étant en lecture seule pendant une restauration.

- Si les fichiers ont un utilisateur propriétaire autre que vous.

- Si votre fichier est ouvert dans un autre programme.

- Si le fichier se trouve sur un périphérique, vous avez atteint la limite d’un maximum de 256 fichiers à la racine du périphérique. Dans ce cas, vous devez reformater le périphérique/la carte SD référencé avec un système de fichiers FAT32, ou supprimer des fichiers de la mémoire de la carte SD/du périphérique.

Vous devez corriger la cause sous-jacente à l’accès refusé avant de reprendre l’utilisation de calibre. Lisez le message d’erreur avec soin, repérez le fichier en cause et rétablissez les permissions d’accès sur ce fichier ou sur les répertoires qui le contienne

Puis-je avoir la métadonnée commentaire apparaître sur ma liseuse ?

La plupart des liseuses ne supportent pas cela. Vous devriez vous plaindre auprès du fabricant à ce sujet et j’espère que si suffisamment de gens se plaignent, les choses vont changer. En attendant, vous pouvez insérer des métadonnées, y compris des commentaires dans une « Page jaquette » au début du livre numérique, en utilisant l’option « Insérer les métadonnées comme page au début du livre » durant la conversion. L’option se trouve dans la section Détection de la structure des paramètres de conversion. Pour que cela ait un effet, vous devez convertir le livre. Si votre livre est déjà dans un format qui ne nécessite pas de conversion, vous pouvez convertir de ce format sur le même format.

Une autre alternative est de créer un catalogue sous forme de livre numérique contenant une liste de tous les livres dans votre bibliothèque calibre, avec leurs métadonnées. Cliquez et maintenez le bouton Convertir pour accéder à l’outil de création de catalogues. Et avant que vous demandiez, non, vous ne pouvez pas avoir de lien direct du catalogue aux livres sur votre liseuse.

Comment obtenir que calibre utilise mon proxy HTTP ?

Par défaut, calibre utilise tous les paramètres de proxy définis dans votre OS. Parfois, ceux-ci sont incorrects, par exemple, sous Windows, si vous n’utilisez pas Microsoft Edge alors les paramètres de proxy peuvent ne pas être à jour. Vous pouvez dire à calibre pour utiliser un serveur proxy notamment en réglant les variables d’environnement `` http_proxy`` et `` https_proxy``. Le format de la variable est : http://username:password@servername vous devriez demander à votre administrateur réseau de vous donner la valeur correcte pour cette variable. Notez que calibre ne prend en charge que les proxies HTTP et non les proxies SOCKS. Vous pouvez voir les proxies actuellement utilisés par calibre dans Préférences->Divers.

Je voudrais certaines fonctionnalités ajoutées à calibre. Que puis-je faire ?

Vous avez deux choix:

1. Créer un patch en bidouillant calibre et me l’envoyer pour examen et inclusion. Voir Développement [https://calibre-ebook.com/fr/get-involved].

2. Ouvrir un bug demandant la fonctionnalité [https://calibre-ebook.com/fr/bugs]. Rappelez-vous que même si vous pensez que votre demande de fonctionnalité est extrêmement importante/essentielle, les développeurs de calibre peuvent ne pas être d’accord. Heureusement, calibre est open source, ce qui signifie que vous avez toujours la possibilité de mettre en œuvre votre fonction vous-même, ou d’embaucher quelqu’un pour le faire pour vous. En outre, calibre a une architecture d’extension compréhensive, vous devriez donc être en mesure de développer votre fonctionnalité en tant qu’extension, voir Écrire vos propres extensions pour étendre les fonctionnalités de calibre.

Pourquoi calibre n’a pas de mise à jour automatique ?

Pour de nombreuses raisons :

- Il n’est pas nécessaire de mettre à jour chaque semaine. Si vous êtes satisfait du fonctionnement de calibre, désactivez la notification de mise à jour. Revenez vérifier pour voir si vous voulez mettre à jour une fois par an ou plus. Il y a une case à cocher pour désactiver la notification de mise à jour, sur la notification de mise à jour elle-même.

- Les téléchargements de calibre utilisent actuellement environ 150 To de bande passante par mois [https://calibre-ebook.com/fr/dynamic/downloads]. L’implémentation des mises à jour automatiques augmenterait considérablement cela et cela finirait par coûter des milliers de dollars par mois, que quelqu’un devrait payer.

- Si j’implémentais une boîte de dialogue permettant de télécharger la mise à jour et de la lancer, au lieu d’aller sur le site comme il faut le faire actuellement, cela permettrait d’économiser tout au plus cinq clics par semaine. Il y a de bien plus hautes priorités à mettre en place pour le développement de calibre.

- Si vous détestez vraiment télécharger la mise à jour calibre toutes les semaines mais tenez à avoir la dernière version, je vous encourage à l’exécuter depuis la source qui rend la mise à jour insignifiante. Les instructions sont disponibles ici.

- Il y a des installateurs de mise à jour automatiques faits par des utilisateurs calibre dans le forum calibre [https://www.mobileread.com/forums/forumdisplay.php?f=238].

Quelle est la licence de calibre ?

calibre est sous licence General Public License v3 (une licence open source). Cela signifie que vous êtes libre de redistribuer calibre tant que vous rendez le code source disponible. Donc, si vous voulez mettre calibre sur un CD avec votre produit, vous devez également mettre le code source de calibre sur le CD. Le code source est disponible en téléchargement [https://download.calibre-ebook.com]. Vous êtes libre d’utiliser le résultat des conversions de calibre comme vous le souhaitez. Vous ne pouvez pas utiliser le code et les bibliothèques de calibre dans votre logiciel sans rendre votre logiciel open source. Pour plus de détails, voir The GNU GPL v3 [https://www.gnu.org/licenses/gpl.html].

Comment exécuter calibre depuis ma clé USB ?

Une version portable de calibre est disponible ici [https://calibre-ebook.com/fr/download_portable].

Comment faire fonctionner des éléments de calibre comme le téléchargement d’actualités et le serveur de contenu sur mon propre serveur Linux ?

Tout d’abord, vous devez installer calibre sur votre serveur linux. Si votre serveur utilise une distribution Linux récente, vous ne devriez avoir aucun problème pour y installer calibre.

Note
calibre nécessite GLIBC >= 2.31 et libstdc++ >= 6.0.28. Si vous disposez d’un serveur plus ancien, vous devrez soit les compiler à partir des sources, soit utiliser calibre 3.48 qui nécessite GLIBC >= 2.17 ou 2.85.1 qui nécessite le GLIBC >= 2.13 ou calibre 1.48 qui ne nécessite que GLIBC >= 2.10. En outre, bien que les utilitaires en ligne de commande de calibre n’aient pas besoin d’un serveur X en fonctionnement, certains d’entre eux exigent que les bibliothèques du serveur X soient installées sur votre système. Cela est dû à Qt, qui est utilisé pour diverses tâches de traitement d’images, et aux liens avec ces bibliothèques. Si vous obtenez une ImportError à propos de certains modules Qt, il est probable qu’il vous manque certaines bibliothèques X. Les candidats typiques sont : libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0.

Vous pouvez exécuter le serveur de calibre via la commande:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

Vous pouvez télécharger les actualités et les convertir en livre numérique avec la commande:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

Si vous voulez générer un MOBI, utilisez outputfile.mobi à la place et utiliser --output-profile kindle.

Vous pouvez envoyer par courriel les actualités téléchargées avec la commande:

/opt/calibre/calibre-smtp

Je vous laisse trouver la ligne de commande exacte comme un exercice pour le lecteur.

Enfin, vous pouvez ajouter les nouvelles téléchargées vers la bibliothèque calibre avec:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

N’oubliez pas de lire la section Interface en ligne de commande du Manuel de l’Utilisateur calibre pour en savoir sur celles-ci et sur d’autres commandes.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels

Tutoriels

Vous trouverez ici des tutoriels pour vous guider dans des fonctionnalités plus avancées de calibre, telles que XPath et les modèles.

- Ajouter votre site web favori d’actualités

- Gérer les sous-groupes de livres, par exemple « genre »

- Tutoriel XPath

- Le langage de modèle calibre

- Tout à propos de l’utilisation des expressions régulières dans calibre

- Écrire vos propres extensions pour étendre les fonctionnalités de calibre

- Compositions mathématiques dans des livres numériques

- Création de catalogues AZW3 • EPUB • MOBI

- Bibliothèques virtuelles

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »

- Gérer les sous-groupes de livres, par exemple « genre »

Gérer les sous-groupes de livres, par exemple « genre »

Certaines personnes souhaitent organiser les livres dans leur bibliothèque en sous-groupes, similaires à des sous-répertoires. La raison la plus habituellement donnée est de créer des hiérarchies de genre, mais il y en a beaucoup d’autres. Un utilisateur demandais une manière d’organiser des manuels par sujet et numéro de cours. Un autre voulait garder une trace de cadeaux par sujet et destinataires. Ce tutoriel utilisera l’exemple genre pour le reste de ce poste.

Avant de commencer, merci de noter que nous ne parlons pas de répertoires sur le disque dur. Les sous-groupes ne sont pas des répertoires de fichiers. Les livres ne seront copiés nul part. La structure de fichier de la bibliothèque calibre n’est pas affectée. Au lieu de cela, nous faisons une présentation d’une manière d’organiser et d’afficher des sous-groupes de livres dans une bibliothèque calibre.

- Configuration

- Recherche

- Restrictions

- Modèles de fonctions utiles

Les requis habituellement donnés pour les sous-groupes comme les genres sont :

- Un sous-groupe (par ex., un genre) doit contenir (pointer vers) des livres, pas des catégories de livres. C’est ce qui distingue les sous-groupes des catégories utilisateur calibre.

- Un livre peut être dans de multiples sous-groupes (genres). C’est ce qui distingue les sous groupes des répertoires de fichier physiques.

- Les sous-groupes (genres) doivent former une hiérarchie; les sous-groupes peuvent contenir des sous-groupes.

Les étiquettes vous donnent les deux premières. Si vous étiquetez un livre avec le genre alors vous pouvez utiliser le Navigateur d’étiquettes (ou la recherche) pour trouver les livres avec ce genre, vous donnant la première. Beaucoup de livres peuvent avoir la/les même(s) étiquette(s), vous donnant la deuxième. Le problème est que les étiquettes ne satisfont pas le troisième prérequis. Elles ne procurent pas une hiérarchie.

La fonctionnalité hiérarchie de calibre vous donne la troisième – la capacité de voir les genres dans un “arbre” et la capacité de facilement rechercher des livres dans genre ou sous-genre. Par exemple, supposons que votre structure de genre est similaire à ceci

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

En utilisant la caractéristique hiérarchie, vous pouvez voir ces genres dans le navigateur d’étiquettes sous la forme d’un arbre, comme montré dans la capture d’écran. Dans cet exemple, le niveau intrinsèque (Genre) est une colonne personnalisée qui contient les genres. Les genres contenant des sous genres apparaissent avec un petit triangle à leur suite. Cliquer sur ce triangle ouvrira l’élément et affichera les sous genres, comme vous pouvez le voir avec Histoire et Science Fiction.

Cliquer sur un genre peut rechercher tous les livres avec ce genre ou les descendants de ce genre. Par exemple, en cliquant sur Science Fiction vous aurez accès aux trois genres enfants de Alternate History, Military et Space Opera. Cliquer sur Alternate History donnera accès aux livres correspondant à ce genre précis, ignorant ceux qui sont dans Military et Space Opera. Bien sûr, un livre peut avoir plusieurs genres. Si un livre est à la fois Space Opera et Militaire, alors vous verrez ce livre en cliquant sur l’un ou l’autre genre. « Rechercher » est abordé plus en détail ci dessous.

Une autre chose que vous pouvez voir dans l’image est que le genre Military apparaît deux fois, une fois sous History et une fois sous Science Fiction. Parce que les genres sont dans une hiérarchie, ceux-ci sont deux genres séparés. Un livre peut être dans un, l’autre ou les deux (discutable dans ce cas). Par exemple, les livres « The Second World War » de Winston Churchill pourraient être dans « History.Military ». Les Honor Harrington de David Weber pourraient être dans « Science Fiction.Military », et pour cette matière également dans « Science Fiction.Space Opera. »

Une fois qu’un genre existe, c’est qu’il y a au moins un livre de ce genre, vous pouvez facilement l’appliquer à d’autres livres en faisant glisser les livres de la liste de la bibliothèque sur le genre que vous voulez leur donner. Vous pouvez également inclure les livres dans des genres par l’intermédiaire de l’Éditeur de métadonnées; plus sur ceci ci dessous.

Configuration

A présent, votre question peut être : « Comment mettre tout cela en place ? ». Il y a trois étapes : 1) Créer la colonne personnalisée, 2) Dire à calibre que la nouvelle colonne doit être traitée comme une hiérarchie, et 3) ajouter les genres.

Vous créez la colonne personnalisée par la voie habituelle, en utilisant Préférences–>Ajout de colonnes personnalisées. Cet exemple utilise « #genre » comme nom de recherche et « Genre » comme en-tête de colonne. Le type de colonne est « Texte séparé par des virgules, comme étiquettes, affichée dans le navigateur d’étiquettes. »

Après avoir redémarrer calibre, vous devez dire à calibre que la colonne est à être traitée comme une hiérarchie. Allez dans Préférences → Apparence → Navigateur d’étiquettes et entrez le nom de recherche « #genre » dans la case « Catégories avec des articles hiérarchiques ». Cliquez sur Appliquer, et vous en avez fini de mettre en place.

En ce moment, il n’y a aucun genre dans la colonne. Nous nous retrouvons avec la dernière étape : comment appliquer un genre à un livre. Un genre n’existe pas dans calibre jusqu’à ce qu’il apparaisse sur au moins un livre. Pour apprendre comment appliquer un genre pour la première fois, nous devons entrer dans certains détails au sujet de à quoi ressemble un genre dans les métadonnées d’un livre.

Une hiérarchie de “choses” est construite en créant un élément consistant en expressions séparées par des points. En continuant l’exemple genre, ces éléments seraient « History.Military », « Mysteries.Vampire », « Science Fiction.Space Opera », etc. Donc pour créer un nouveau genre, vous prenez un livre qui devrait avoir ce genre, éditez ses métadonnées, et entrez le nouveau genre dans la colonne que vous avez créée. En continuant notre exemple, si vous voulez attribuer un nouveau genre « Comics » avec un sous genre « Superheroes » à un livre, vous devrez “éditer les métadonnées” pour ce livre(bande dessinée), choisir l’onglet Métadonnées personnalisées, et entrer « Comics.Superheroes » comme affiché ici (ignorez les autres colonnes personnalisées) :

Après avoir fait ce qui est indiqué plus haut, vous voyez dans le Navigateur d’étiquettes :

A partir d’ici, pour appliquer ce nouveau genre à un livre (une bande dessinée, vraisemblablement), vous pouvez soit déposer le livre sur le genre, ou l’ajouter au livre en utilisant éditer les métadonnées exactement de la même manière que plus haut.

Note

L’affichage hiérarchique fonctionne uniquement si le Navigateur d’étiquettes est paramétré pour trier les éléments par nom. C’est l’option par défaut et elle peut être vérifiée en cliquant le bouton Configurer en bas du Navigateur d’étiquettes.

Recherche

La meilleure façon de chercher des genres est d’utiliser le navigateur d’étiquettes, de cliquer sur le genre que vous souhaitez voir. En cliquant sur un genre qui a des descendants, vous verrez tous les livres contenus dans ce genre et dans tous les descendants de ce genre. Cependant, ceci peut évoquer une question. Juste parce qu’un genre a des descendants ne signifie pas que ce n’est pas un genre de son propre chef. Par exemple, un livre peut avoir le genre « History » mais pas « History.Military ». Comment recherchez-vous des livres avec seulement « History » ?

Le mécanisme de recherche du Navigateur d’étiquettes sait si un élément a des sous éléments. S’il le fait, cliquer sur l’élément permet d’accéder à 5 variations de recherche au lieu de trois. Le premier est le plus vert normal, qui vous affiche les livres avec ce genre uniquement (par ex. History). Le second est le double plus (montré plus haut), qui vous affiche les livres avec ce genre et tous les sous genres (par ex. History et History.Military). Le troisième est le moins rouge normal, qui vous affiche les livres qui n’ont pas ce genre avec exactitude, Le quatrième un double moins, qui montre les livres sans ce genre ou les sous genres. Le cinquième est revenir au début, pas de marque, signifie pas de recherche.

Restrictions

Si vous recherchez un genre, créez une recherche sauvegardée pour celui-ci, vous pouvez utiliser la fonction Restriction pour créer une bibliothèque virtuelle contenant les livres de ce genre. C’est très utile si vous voulez faire d’autres recherches à l’intérieur du genre ou gérer/mettre à jour les métadonnées des livres dans le genre. Poursuivant notre exemple, vous pouvez créer une recherche sauvegardée nommée “History.Japanese” en cliquant d’abord sur le sous-genre Japanese dans le Navigateur d’étiquettes pour obtenir une recherche dans la barre de recherche, puis en entrant History.Japanese dans la boîte de Recherches sauvegardées, puis en appuyant sur le bouton « Enregistrer la recherche » (le carré vert avec un signe plus blanc du côté droit).

Après avoir créé la recherche sauvegardée, vous pouvez l’utiliser comme restriction.

Modèles de fonctions utiles

Vous pourriez vouloir employer l’information de genre dans un modèle, comme avec Enregistrer sous ou Envoyer au périphérique . La question pourrait alors être « Comment est-ce que j’obtiens le nom ou les noms périphériques de ce genre ? » Un modèle de fonction calibre, sous-articles, est fournie pour faire cela facilement.

Par exemple, supposons que vous voulez ajouter le niveau de genre le plus périphérique au modèle save-to-disk pour faire des répertoires genre, comme dans « History/The Gathering Storm - Churchill, Winston ». Pour faire cela, vous devez extraire le premier niveau de la hiérarchie et l’ajouter au début avec une barre oblique pour indiquer qu’il devra en faire un répertoire. Le modéle ci-dessous accomplis cela:

{#genre:subitems(0,1)||/}{title} - {authors}

Voir Le langage des modèles pour plus d’informations sur les modèles et la fonction subitems().

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »
- Tutoriel XPath

Tutoriel XPath

Dans ce tutoriel, il vous sera donné une légère introduction à XPath [https://fr.wikipedia.org/wiki/XPath], un langage de requête qui peut être utilisé pour sélectionner des parties quelconques de documents HTML [https://fr.wikipedia.org/wiki/HTML] dans calibre. XPath est un standard utilisé largement, et faire une recherche sur Google produira une tonne d’information. Ce tutoriel, toutefois, se concentrera sur l’utilisation d’XPath pour des tâches relatives aux livres numériques comme trouver les entêtes de chapitre dans un document HTML non structuré.

Contenu

- Sélection par nom de balise

- Sélection par attributs

- Sélection par contenu de balise

- Exemple de livre numérique

- Les fonctions XPath intégrées

Sélection par nom de balise

La forme la plus simple de sélection est de sélectionner les balises par nom. Par exemple, supposons que vous voulez sélectionner toutes les balises <h2> dans un document. La requête XPath pour cela est simplement

//h:h2 (Selects all <h2> tags)

Le préfixe //`signifie *rechercher à n’importe quel niveau du document*. Maintenant supposons que vous voulez rechercher les balises ``<span>` qui sont contenues dans des balises <a>. Ceci peut être accompli avec

//h:a/h:span (Selects <span> tags inside <a> tags)

Si vous voulez rechercher des balises à un niveau particulier dans le document, changez le préfixe

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
children of the <body> tag)

Ceci correspondra uniquement à <p>Un livre numérique très court pour démontrer l'utilisation d'XPath.</p> dans le Exemple de livre numérique mais dans aucune des autres balises <p>. Le préfixe h: dans l’exemple plus haut est nécessaire pour correspondre aux balises XHTML. Ceci car, en interne, calibre représente tout le contenu comme du XHTML. En XHTML les balises ont un espace de nom, et h: est le préfixe d’espace de nom pour les balises HTML.

Maintenant supposons que vous voulez sélectionner conjointement les balises <h1> et <h2>. Pour faire cela, nous avons besoin d’une construction XPath appelée prédicat. Un prédicat est simplement un test qui est utilisé pour sélectionner les balises. Les tests peuvent être arbitrairement puissants et au long de la progression de ce tutoriel, vous verrez des exemples plus puissants. Un prédicat est créé en mettant l’expression test entre crochets.

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

Il y a plusieurs nouvelles caractéristiques dans cette expression XPath. La première est l’utilisation du joker *. Il signifie correspond à n’importe quelle balise. Maintenant regardez l’expression test name()='h1' or name()='h2'. name() est un exemple d’une fonction intégrée. Il évalue simplement le nom de la balise. Donc en l’utilisant, nous pouvons sélectionner des balises dont les noms sont soit h1 ou h2. Notez que la fonction name() ignore les espaces de nom de sorte qu’il n’est d’aucune nécessité du préfixe h:. XPath a plusieurs fonctions intégrées utiles. Quelques unes de plus seront introduites dans ce tutoriel.

Sélection par attributs

Pour sélectionner des balises sur base de leurs attributs, l’utilisation des prédicats est requise //

//*[@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")

Ici l’opérateur @ réfère aux attributs de la balise. Vous pouvez utiliser certaines des Fonctions intégrées XPath pour effectuer des correspondances plus sophistiquées sur les valeurs attribut.

Sélection par contenu de balise

En utilisant XPath, vous pouvez même sélectionner des balises basées sur le texte qu’elles contiennent. La meilleure manière de faire cela est d’utiliser la puissance des expressions régulières par l’intermédiaire de la fonction intégrée re:test():

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

Ici l’opérateur . réfère au contenu de la balise, tout comme l’opérateur @ réfère à ses attributs.

Exemple de livre numérique

<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>

Les fonctions XPath intégrées

name()

Le nom de la balise actuelle.

contains()

contains(s1, s2) renvoie true si s1 contient s2.

re:test()

re:test(src, pattern, flags) renvoie true si la chaîne src correspond au pattern de l’expression régulière. un indicateur particulièrement utile est i, il rend les correspondances sensibles à la casse. Une bonne amorce sur la syntaxe pour les expressions régulières peut être trouvée sur regexp syntax [https://docs.python.org/library/re.html]

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »
- Le langage de modèle calibre

Le langage de modèle calibre

Le langage de modèle de calibre est un langage spécifique à calibre utilisé dans l’ensemble de calibre pour des tâches telles que la spécification des chemins de fichiers, le formatage des valeurs et le calcul de la valeur des colonnes spécifiées par l’utilisateur. Exemples :

- Spécifier la structure du dossier et les noms de fichiers lors de l’enregistrement des fichiers de la bibliothèque calibre sur le disque ou le lecteur de livres numériques.

- Définir des règles pour l’ajout d’icônes et de couleurs à la liste de livres de calibre.

- Définir les colonnes virtuelles qui contiennent des données provenant d’autres colonnes.

- Recherche avancée dans la bibliothèque.

- Recherche et remplacement avancé des métadonnées.

Le langage est construit autour de la notion de modèle, qui spécifie les métadonnées du livre à utiliser, les calculs sur ces métadonnées, et la façon dont elles doivent être formatées.

Modèles de base

Un modèle de base est constitué d’une ou plusieurs expression modèle. Une expression modèle consiste en du texte et des noms entre crochets ({}) qui sont remplacés par les métadonnées correspondantes du livre en cours de traitement. Par exemple, le modèle par défaut de calibre utilisé pour enregistrer des livres sur un périphérique comporte 4 expressions modèle:

{author_sort}/{title}/{title} - {authors}

Pour le livre « The Foundation » par « Isaac Asimov » cela donnera:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

Les barres obliques ne sont pas des expressions modèle car elles se trouvent entre les {}. Un tel texte est laissé là où il apparaît. Par exemple, si le modèle est:

{author_sort} Some Important Text {title}/{title} - {authors}

dès lors, pour « The Foundation », le modèle produit:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

Une expression modèle peut accéder à toutes les métadonnées disponibles dans calibre, y compris les colonnes personnalisées (colonnes que vous créez vous-même), en utilisant le nom de recherche d’une colonne. Pour trouver le nom de recherche d’une colonne (parfois appelée champs), passez votre souris sur l’en-tête de la colonne dans la liste des livres de calibre. Les noms de recherche pour les colonnes personnalisées commencent toujours par #. Pour les colonnes de type série, il y a un champ supplémentaire appelé #lookup name_index qui est l’index de la série pour ce livre dans la série. Par exemple, si vous avez une colonne de série personnalisée nommée #messéries, il y aura également une colonne nommée #messéries_index. L’index de la colonne de série standard est nommé series_index.

En plus des champs standard basés sur des colonnes, vous pouvez également utiliser :

- {formats} - Une liste des formats disponibles dans la bibliothèque calibre pour un livre

- {identifiers:select(isbn)} - L’ISBN du livre

Si les métadonnées du champ pour un livre donné ne sont pas définies, le champ dans le modèle est remplacé par la chaîne vide (''). Par exemple, considérons le modèle suivant

{author_sort}/{series}/{title} {series_index}

Si le livre d’Asimov « Seconde Fondation » fait partie de la série « Fondation », alors le modèle produit

Asimov, Isaac/Foundation/Second Foundation 3

Si aucune série n’a été saisie pour le livre, le modèle produit

Asimov, Isaac/Second Foundation

Le processeur de modèles supprime automatiquement les barres obliques multiples et les espaces avant ou arrière.

Mise en forme avancée

Outre la substitution de métadonnées, les modèles peuvent inclure du texte supplémentaire de manière conditionnelle et contrôler le formatage des données substituées.

Comprenant conditionnellement le texte

Parfois, vous voulez que du texte n’apparaisse dans la sortie que si un champ n’est pas vide. Un cas courant est celui de series et series_index où vous voulez soit rien, soit les deux valeurs séparées par un trait d’union. calibre gère ce cas en utilisant une syntaxe spéciale de expression modèle.

Par exemple, et en utilisant l’exemple de Fondation ci-dessus, supposons que vous voulez que le modèle produise Fondation - 3 - Deuxième Fondation. Ce modèle produit ce résultat :

{series} - {series_index} - {title}

Cependant, si un livre n’a pas de série, le modèle produira - - le titre, ce qui n’est probablement pas ce que vous voulez. En général, les gens veulent que le résultat soit le titre sans les traits d’union superflus. Vous pouvez y parvenir en utilisant la syntaxe de modèle suivante :

{field:|prefix_text|suffix_text}

Cette expression modèle indique que si champ a la valeur XXXX, le résultat sera prefix_textXXXXXsuffix_text. Si champ est vide (n’a pas de valeur) alors le résultat sera la chaîne vide (rien) car le préfixe et le suffixe sont ignorés. Le préfixe et le suffixe peuvent contenir des blancs.

Ne pas utiliser de sous-modèles (`{ … }`) ou de fonctions (voir ci-dessous) dans le préfixe ou le suffixe.

En utilisant cette syntaxe, nous pouvons résoudre le problème de non-série ci-dessus avec le modèle

{series}{series_index:| - | - }{title}

Les traits d’union ne seront inclus que si le livre possède un index de série, ce qui n’est le cas que si le livre possède une série. Si l’on reprend l’exemple de Fondation, le modèle produira Fondation - 1 - Deuxième Fondation.

Notes :

- Vous devez inclure les deux points après le nom de la recherche si vous utilisez un préfixe ou un suffixe.

- Vous devez utiliser soit aucun des caractères |, soit les deux. L’utilisation d’un seul caractère, comme dans {field:| - }, n’est pas autorisée.

- Il est possible de ne fournir aucun texte pour le préfixe ou le suffixe, comme dans {série:|| - }. Le modèle {title:||} est le même que {title}.

Formatage

Supposons que vous vouliez que series_index soit formaté en trois chiffres avec des zéros en tête. Ceci fait l’affaire :

{series_index:0>3s} - Trois chiffres avec des zéros en tête

Pour des zéros qui suivent, utilisez:

{series_index:0>3s} - Trois chiffres avec des zéros qui suivent

Si vous utilisez des indices de série avec des valeurs fractionnées, par exemple 1.1, vous pouvez souhaiter que les points décimaux soient alignés. Par exemple, vous pouvez vouloir que les indices 1 et 2.5 apparaissent sous la forme 01.00 et 02.50 afin qu’ils soient triés correctement sur un périphérique qui effectue un tri lexical. Pour ce faire, utilisez :

{series_index:0>5.2f} - Cinq caractères composés de deux chiffres avec des zéros en tête, un point décimal, puis deux chiffres après le point décimal.

Si vous ne voulez que les deux premières lettres de la donnée, utilisez :

{author_sort :.2} - Seulement les deux premières lettres du nom de tri de l’auteur

Une grande partie du formatage du langage de modèle de calibre provient de Python. Pour plus de détails sur la syntaxe de ces opérations de formatage avancées, voir la documentation Python <https://docs.python.org/3/library/string.html#formatstrings>

Utilisation de modèles pour définir des colonnes personnalisées

Les modèles peuvent être utilisés pour afficher des informations qui ne sont pas dans les métadonnées de calibre, ou pour afficher les métadonnées différemment du format normal de calibre. Par exemple, vous pourriez vouloir afficher le ISBN, un champ que calibre n’affiche pas. Pour cela, vous pouvez créer une colonne personnalisée de type Colonne construite à partir d’autres colonnes (appelée ci-après colonnes composites) et fournir un modèle pour générer le texte affiché. La colonne affichera le résultat de l’évaluation du modèle. Par exemple, pour afficher l’ISBN, créez la colonne et entrez « identifiers:select(isbn)} » dans la champ du modèle. Pour afficher une colonne contenant les valeurs de deux colonnes personnalisées de séries, séparées par une virgule, utilisez {#series1:||,}{#series2}.

Les colonnes composites peuvent utiliser n’importe quelle option modèle, y compris le formatage.

Remarque : Vous ne pouvez pas modifier les données affichées dans une colonne composite. Vous devez plutôt modifier les colonnes sources. Si vous modifiez une colonne composite, par exemple en double-cliquant dessus, calibre ouvrira le modèle à modifier, et non les données sous-jacentes.

Modèles et tableaux de connexions

Les tableaux de connexions sont utilisés pour modifier les métadonnées écrites dans les livres pendant l’envoi à une liseuse et les opérations de sauvegarde sur le disque. Le tableau de connexion vous permet de spécifier pour un modèle de lecteur précis quelles données à insérer dans les métadonnées du livre. Vous pouvez utiliser les tableaux de connexions pour modifier les champs suivants : authors, author_sort, language, publisher, tags, title, title_sort. Cette fonctionnalité aide ceux qui veulent utiliser les différentes métadonnées des livres pour contrôler le tri ou l’affichage des livres sur leurs périphériques.

Lorsque vous créez un tableau de connexion, vous spécifiez le format et le périphérique pour lequel le tableau de connexion doit être utilisé. Un périphérique spécial est procuré, save_to_disk qui est utilisé lors de la sauvegarde de formats (en opposition à les envoyer vers le périphérique). Une fois que vous avez choisi le format et le périphérique, vous choisissez les champs de métadonnées à modifier, indiquez le modèle à appliquer pour fournir les nouvelles valeurs. Ces modèles sont reliés à leurs champs de destination, d’où le nom de tableau de connexions. Vous pouvez bien sûr utiliser des colonnes composites dans ces modèles.

Les tableaux de connexions sont assez flexibles et peuvent être écrits en mode fonction unique, en mode programme modèle, en mode programme général ou en mode modèle Python.

Quand un tableau de connexion doit s’appliquer (Serveur de contenu, sauvegarde sur disque ou envoyer au périphérique), calibre recherche les tableau de connexions définis et celui qui est correct pour le format donné et le périphérique. Par exemple, pour trouver le tableau de connexion approprié pour un livre EPUB envoyé à un périphérique ANDROID, calibre recherche dans les tableaux de connexion dans l’ordre de recherche suivant :

- un tableau de connexion avec une correspondance exacte sur le format et le périphérique, par ex., EPUB et ANDROID

- un tableau de connexion avec une correspondance exacte sur le format et le choix spécial n'importe quel périphérique, par ex., EPUB et n'importe quel périphérique

- un tableau de connexion avec le choix spécial n'importe quel format et une correspondance exacte sur le périphérique, par ex., n'importe quel format et ANDROID

- un tableau de conversion avec n'importe quel format et n'importe quel périphérique

Les étiquettes et les champs auteur ont un traitement spécial, parce que ces deux champs peuvent contenir plusieurs données. Un livre peut avoir plusieurs étiquettes et plusieurs auteurs. Lorsque vous spécifiez que l’un de ces deux champs doit être modifié, le résultat du modèle est examiné pour voir s’il y a plus d’un élément dedans. Pour les étiquettes, le résultat est découpé partout où calibre trouve une virgule. Par exemple, si le modèle produit les valeurs Thriller, Horreur, alors ke résultat sera deux étiquettes, Thriller et Horreur. Il n’est pas possible de mettre une virgule au milieu d’une étiquette.

La même chose se produit pour les auteurs, mais en utilisant un caractère différent comme séparateur, le & (esperluette) au lieu d’une virgule. Par exemple, si le modèle produit la valeur Blogs, Joe&Posts, Susan, alors le livre finira avec deux auteurs Blogs, Joe et Posts, Susan. Si le modèle produit la valeur Blogs, Joe;Posts, Susan, alors ce livre aura un auteur avec un nom assez étrange.

Les tableaux de connexions affectent les métadonnées écrites dans le livre quand il est sauvegardé sur le disque ou copié sur le périphérique. Les tableaux de connexions n’affecte pas les métadonnées utilisées par Enregistrer sur le disque et Envoyer au périphérique. A la place, les noms de fichiers sont construits en utilisant les modèles entrés dans la fenêtre de préférence appropriée.

Utilisation des fonctions dans les modèles - Mode de fonctionnement unique

Supposons que vous vouliez afficher la valeur d’un champ en casse majuscules alors que ce champ est normalement en casse titre. Vous pouvez le faire en utilisant les fonctions de modèle. Par exemple, pour afficher le titre en casse majuscules, utilisez la fonction uppercase, comme dans {title:uppercase()}. Pour l’afficher en casse titre, utilisez la fonction {title:titlecase()}.

Les fonctions sont placées dans la partie format du modèle, après le : et avant le premier | ou le } de fermeture si aucun préfixe/suffixe n’est utilisé. Si vous avez à la fois une référence de format et de fonction, la fonction vient après un deuxième :. Les fonctions renvoient la valeur de la colonne spécifiée dans le modèle, convenablement modifiée.

La syntaxe pour utiliser les fonctions est l’une des suivantes

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

Les noms de fonctions doivent toujours être suivis de parenthèses ouvrantes et fermantes. Certaines fonctions nécessitent des valeurs supplémentaires (arguments), qui sont placées à l’intérieur des parenthèses. Les arguments sont séparés par des virgules. Les virgules Littérales (les virgules en tant que texte, et non en tant que séparateurs d’arguments) doivent être précédées d’une barre oblique inversée (\) . Le dernier (ou le seul) argument ne peut pas contenir de parenthèse fermante textuelle.

Les fonctions sont évaluées avant les spécifications de format et le préfixe/suffixe. Voir plus bas pour un exemple d’utilisation à la fois d’un format et d’une fonction.

Important : Si vous avez de l’expérience en programmation, veuillez noter que la syntaxe du Mode Fonction Unique n’est pas celle à laquelle vous vous attendez. Les chaînes de caractères ne sont pas citées et les espaces sont significatifs. Tous les arguments sont considérés comme des constantes ; il n’y a pas d’expressions.

N’utilisez pas les sous-modèles (`{ … }`) comme arguments de fonction. A la place, utilisez plutôt Mode Programme Modèle et Mode Programme Général.

Certaines fonctions nécessitent des expressions régulières. Dans le langage des modèles, la correspondance des expressions régulières est insensible à la casse.

Dans la documentation des fonctions ci-dessous, la notation [quelque chose]* signifie que quelque chose peut être répété zéro fois ou plus. La notation [quelque chose]+ signifie que quelque chose est répété une ou plusieurs fois (doit exister au moins une fois).

Les fonctions destinées à être utilisées en Mode Fonction Unique sont les suivantes :

capitalize() – renvoie la valeur avec la première lettre en majuscule et le reste en minuscule.

contains(pattern, text if match, text if not match) – vérifie si la valeur est couverte par l’expression régulière pattern. Renvoie text if match si le motif correspond à la valeur, sinon renvoie text if no match.

count(separator) – interprète la valeur comme une liste d’éléments séparés par le separator et retourne le nombre d’éléments dans la liste. La plupart des listes utilisent une virgule comme séparateur, mais authors utilise une esperluette (&). Exemples : {tags:count(,)}, {authors:count(&)}. Alias : count(), list_count()

format_number(template) – interprète la valeur comme un nombre et formate ce nombre en utilisant un modèle de formatage Python tel que {0:5.2f} ou {0 :,d}` ou ${0:5,.2f}. Le modèle de formatage doit commencer par {0: et se terminer par } comme dans les exemples ci-dessus. Exception : vous pouvez omettre le début « {0 : » et la fin « } » si le modèle de format ne contient qu’un format. Voir le langage des modèles et la documentation Python [https://docs.python.org/3/library/string.html#formatstrings] pour plus d’exemples. Renvoie la chaîne vide si le formatage échoue.

human_readable() – s’attend que la valeur soit un nombre et renvoie une chaîne représentant ce nombre en KB, MB, GB, etc.

ifempty(text) – si le champ n’est pas vide, renvoie la valeur du champ. Autrement renvoie text if empty.

in_list(separator, [ pattern, found_val, ]* not_found_val) – interprète la valeur comme une liste d’éléments séparés par le separator, en vérifiant le pattern pour chaque élément de la liste. Si le pattern correspond à un élément, il renvoie found_val, sinon il renvoie not_found_val. La paire pattern``et ``found_value peut être répétée autant de fois que souhaité, ce qui permet de renvoyer des valeurs différentes selon la valeur de l’élément. Les motifs sont vérifiés dans l’ordre, et la première correspondance est retournée.

language_strings(localize) – retourne les noms de langue [https://www.loc.gov/standards/iso639-2/php/code_list.php] pour les codes de langue [https://www.loc.gov/standards/iso639-2/php/code_list.php] passés en tant que valeur. Exemple : {languages:language_strings()}. Si localize est zéro, retourne les chaînes de caractères en anglais. Si localize est différent de zéro, retourne les chaînes de caractères dans la langue de la locale courante. Lang_codes est une liste séparée par des virgules.

list_item(index, separator) – interprète la valeur comme une liste d’éléments séparés par un separator, en retournant index'. Le premier élément est le numéro zéro. Le dernier élément a l’indice -1 comme dans list_item(-1,separator). Si l’élément n’est pas dans la liste, alors la chaîne vide est retournée.

lookup([ pattern, key, ]* else_key) – Les motifs seront comparés à la valeur dans l’ordre. Si un motif correspond, la valeur du champ nommé par key est retournée. Si aucun motif ne correspond, la valeur du champ nommé par else_key est retournée. Voir switch (ci-dessous).

lowercase() – renvoie la valeur du champ en minuscule.

rating_to_stars(use_half_stars) – Renvoie la note sous forme de chaîne de caractères étoilés (★). La valeur doit être un nombre entre 0 et 5. Définissez use_half_stars à 1 si vous voulez des caractères de demi-étoile pour les nombres fractionnés disponibles avec les colonnes d’évaluation personnalisées.

re(pattern, replacement) – retourne la valeur après application de l’expression régulière. Toutes les instances de pattern dans la valeur sont remplacées par replacement. Le langage des modèles utilise des Expressions régulières Python [https://docs.python.org/3/library/re.html] insensibles à la casse.

select(key) – interprète la valeur comme une liste d’éléments séparés par des virgules, chaque élément ayant la forme id:value (le format identifiant de calibre). La fonction trouve la première paire dont l’id est égal à key et renvoie la valeur correspondante. Si aucun id ne correspond, la fonction renvoie la chaîne vide.

shorten(left chars, middle text, right chars) – Renvoie une version raccourcie de la valeur, composée de left chars du début de la valeur, suivis de middle text, suivis de right chars de la fin de la valeur. Les « left chars » et les « right chars » doivent être des entiers non négatifs. Exemple : supposons que vous vouliez afficher le titre avec une longueur maximale de 15 caractères. Un modèle qui fait cela est {titre:shorten(9,-,5)}. Pour un livre dont le titre est Ancient English Laws in the Times of Ivanhoe, le résultat sera Ancient E-anhoe : les 9 premiers caractères du titre, un -, puis les 5 derniers caractères. Si la longueur de la valeur est inférieure à left chars + right chars + la longueur du middle text alors la valeur sera retournée inchangée. Par exemple, le titre Le Dôme ne sera pas modifié.

str_in_list(separator, [ string, found_val, ]+ not_found_val) – interprète la valeur comme une liste d’éléments séparés par le separator puis compare string à chaque valeur de la liste. string n’est pas une expression régulière. Si string est égal à n’importe quel élément (en ignorant la casse), alors il renvoie la found_val correspondante. Si string contient des separators, elle est également traitée comme une liste et chaque sous-valeur est vérifiée. Les paires string et found_value peuvent être répétées autant de fois que souhaité, ce qui permet de renvoyer des valeurs différentes selon la valeur de la chaîne. Si aucune des chaînes de caractères ne correspond, le système renvoie not_found_value. Les chaînes de caractères sont vérifiées dans l’ordre. La première correspondance est retournée.

subitems(start_index, end_index) – Cette fonction sépare les listes d’éléments hiérarchiques de type balise, comme les genres. Elle interprète la valeur comme une liste d’éléments de type balise séparés par des virgules, où chaque élément est une liste séparée par des points. Elle retourne une nouvelle liste faite en extrayant de chaque élément les composants de start_index à end_index, puis en fusionnant les résultats ensemble. Les doublons sont supprimés. Le premier sous-élément d’une liste séparée par un point a un indice de zéro. Si un index est négatif, il est compté à partir de la fin de la liste. Dans un cas particulier, un end_index de zéro est supposé être la longueur de la liste.

Exemples :

- Supposons une colonne #genre contenant A.B.C :

- {#genre:subitems(0,1)} renvoie « A »

- {#genre:subitems(0,2)} renvoie « A.B »

- {#genre:subitems(1,0)} renvoie « B.C »

- Supposons une colonne #genre contenant « A.B.C, D.E » :

- {#genre:subitems(0,1)} renvoie « A, D »

- {#genre:subitems(0,2)} renvoie « A.B, D.E »

sublist(start_index, end_index, separator) – interprète la valeur comme une liste d’éléments séparés par le separator, renvoyant une nouvelle liste composée des éléments de start_index à end_index. Le premier élément est le numéro zéro. Si un index est négatif, alors il compte à partir de la fin de la liste. Dans un cas spécial, un end_index de zéro est supposé être la longueur de la liste.

Exemples en supposant que la colonne des étiquettes (qui est séparée par des virgules) contient « A, B ,C » :

- {tags:sublist(0,1,\,)} renvoie « A »

- {tags:sublist(-1,0,\,)} renvoie « C »

- {tags:sublist(0,-1,\,)} renvoie « A, B »

swap_around_articles(separator) – renvoie la valeur avec les articles déplacés à la fin. La valeur peut être une liste, dans ce cas chaque article de la liste est traité. Si la valeur est une liste, vous devez fournir le séparateur. Si aucun séparateur n’est fourni, alors la valeur est traitée comme étant une valeur unique, et non une liste. Les articles sont ceux utilisés par calibre pour générer le title_sort.

swap_around_comma() – étant donné une valeur de la forme B, A, retourne A B. Ceci est très utile pour convertir des noms au format LN, FN en FN LN. S’il n’y a pas de virgule dans la valeur, la fonction retourne la valeur inchangée.

switch([pattern, value,]+ else_value) – pour chaque paire pattern, value, vérifie si la valeur correspond à l’expression régulière pattern et si oui, retourne la value associée. Si aucun pattern ne correspond, else_value est retourné. Vous pouvez avoir autant de couples pattern, value que vous le souhaitez. La première correspondance est retournée.

test(text if not empty, text if empty) – retourne text if not empty si la valeur n’est pas vide, sinon retourne text if empty.

titlecase() – renvoie la valeur du champ en casse titre.

transliterate() - Restitue une chaîne en alphabet latin représentant approximativement le son des mots de la chaîne source. Par exemple, si le champ source est Фёдор Миха́йлович Достоевский la fonction renverra Fiodor Mikhailovich Dostoievskii.”

uppercase() – renvoie la valeur du champ en majuscule.

Utilisation des fonctions et du formatage dans le même modèle

Supposons que vous ayez une colonne personnalisée de nombres entiers, ##myint, que vous voulez afficher avec des zéros en tête, comme dans 003. Une façon de le faire est d’utiliser le format 0>3s. Cependant, par défaut, si un nombre (entier ou flottant) est égal à zéro, la valeur est affichée sous la forme d’une chaîne vide, de sorte que les valeurs nulles produiront la chaîne vide, et non 000. Si vous voulez voir les valeurs 000, vous devez utiliser à la fois la chaîne de format et la fonction ifempty pour transformer la valeur vide en zéro. Le modèle serait

{#myint:0>3s:ifempty(0)}

Notez que vous pouvez également utiliser le préfixe et le suffixe. Si vous voulez que le numéro apparaisse sous la forme [003] ou [000], utilisez alors le modèle

{#myint:0>3s:ifempty(0)|[|]}

Mode Programme Général

Le Mode Programme Général (MPG) remplace les expressions modèles par un programme écrit dans le langage modèle. La syntaxe de ce langage est définie par la grammaire suivante
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

Notes :

- une top_expression a toujours une valeur. La valeur d’une expression_list est la valeur de la dernière top_expression Par exemple, la valeur de la liste d’expressions 1;2;'foobar';3 est 3.

- Dans un contexte logique, toute valeur non vide est True.

- Dans un contexte logique, la valeur vide est False.

- Les chaînes de caractères et les nombres peuvent être utilisés indifféremment. Par exemple, 10 et '10' sont la même chose.

- Les commentaires sont des lignes commençant par le caractère « # ». Les commentaires commençant plus loin dans une ligne ne sont pas pris en charge.

Préséance de l’opérateur

L’ordre de préséance de l’opérateurs (ordre d’évaluation), du plus élevé (évalué en premier) au plus bas (évalué en dernier), est le suivant :

- Appels de fonctions, constantes, expressions entre parenthèses, expressions d’instructions, expressions d’affectation, références de champs.

- Les opérateurs unaires plus (+) et moins (-). Ces opérateurs s’évaluent de droite à gauche.

Ces opérateurs et tous les autres opérateurs arithmétiques renvoient des entiers si l’expression donne une partie fractionnaire égale à zéro. Par exemple, si une expression renvoie 3.0, elle est transformée en 3.

- Multiplier (*) et diviser (/). Ces opérateurs sont associatifs et s’évaluent de gauche à droite. Utilisez les parenthèses si vous souhaitez modifier l’ordre d’évaluation.

- Additionner (+) et soustraire (-). Ces opérateurs sont associatifs et s’évaluent de gauche à droite.

- Comparaisons de nombres et de chaînes de caractères. Ces opérateurs renvoient '1' si la comparaison réussit, sinon la chaîne vide (''). Les comparaisons ne sont pas associatives : a < b < c est une erreur de syntaxe.

- Concaténation de chaînes de caractères (&). L’opérateur & renvoie une chaîne de caractères formée par la concaténation des expressions de gauche et de droite. Exemple : «  “aaa” & “bbb”`` renvoie «  “aaabbb”``. L’opérateur est associatif et s’évalue de gauche à droite.

- Unaire non logique (!). Cet opérateur renvoie '1' si l’expression est fausse (évaluée à la chaîne vide), sinon ''.

- L’opérateur logique et (&&). Cet opérateur renvoie “1” si les deux expressions de gauche et de droite sont vraies, ou la chaîne vide '' si l’une ou l’autre est fausse. Il est associatif, s’évalue de gauche à droite, et fait du court-circuitage [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html].

- Logique ou (||). Cet opérateur renvoie '1' si l’expression de gauche ou de droite est vraie, ou '' si les deux sont fausses. Il est associatif, s’évalue de gauche à droite, et fait du court-circuitage [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html]. C’est un or inclusif, qui renvoie « 1 » si les deux expressions de gauche et de droite sont vraies.

Références des champs

Un « field_reference » est évalué à la valeur du champ de métadonnées nommé par le nom de la consultation qui suit le « $ » ou le « $$ ». L’utilisation de $ équivaut à l’utilisation de la fonction field(). L’utilisation de $$ équivaut à l’utilisation de la fonction raw_field. Exemples

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

Expressions if

Les expressions If évaluent d’abord la condition. Si la condition est True (une valeur non vide), alors expression_list de la clause then est évaluée. Si elle est False, expression_list de la clause elif ou else est évaluée si elle est présente. Les parties if et else sont facultatives. Les mots if ``, ``then, elif ``, ``else et fi sont réservés ; vous ne pouvez pas les utiliser comme noms d’identifiants. Vous pouvez mettre des retours à la ligne et des espaces partout où cela a un sens. La condition est une top_expression et non une expression_list ; les points-virgules ne sont pas autorisés. Les expression_list sont des séquences de top_expressions séparées par des points-virgules. Une expression if renvoie le résultat de la dernière top_expressions de la top_expression évaluée, ou la chaîne vide si aucune liste d’expression n’a été évaluée.

Exemples:

* 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)

Exemple de if imbriqué:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

Comme dit plus haut, un if produit une valeur. Cela signifie que tous les cas suivants sont équivalents

* 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

Comme dernier exemple, ce programme retourne la valeur de la colonne series si le livre a une série, sinon la valeur de la colonne title

program: field(if field('series') then 'series' else 'title' fi)

Expressions for

L’expression for itère sur une liste de valeurs, en les traitant une par une. L’expression « list_expression » doit être évaluée soit par un nom de champ de métadonnées, par exemple tags ou #genre, soit par une liste de valeurs. Si le résultat est un lookup name valide, alors la valeur du champ est récupérée et le séparateur spécifié pour ce type de champ est utilisé. Si le résultat n’est pas un nom de référence valide, il est supposé être une liste de valeurs. La liste est supposée être séparée par des virgules, sauf si le mot-clé optionnel separator est fourni, auquel cas les valeurs de la liste doivent être séparées par le résultat de l’évaluation de l’expression separator_expr. Un séparateur ne peut pas être utilisé si la liste est générée par range(). Chaque valeur de la liste est assignée à la variable spécifiée puis la expression_list est évaluée. Vous pouvez utiliser break pour sortir de la boucle, et continue pour revenir au début de la boucle pour la prochaine itération.

Exemple : Ce modèle supprime le premier nom hiérarchique pour chaque valeur dans le genre (#genre), en construisant une liste avec les nouveaux noms

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

Si le Genre original est Histoire.Militaire, Science Fiction.Histoire alternative, Lisez-moi, alors le modèle renvoie Militaire, Histoire alternative, Lisez-moi. Vous pouvez utiliser ce modèle dans le calibre Modifier les métadonnées par lot  →  Rechercher & remplacer avec Rechercher après défini à modèe pour supprimer le premier niveau de la hiérarchie et attribuer la valeur résultante à Genre.

Note : la dernière ligne du modèle, new_tags, n’est pas strictement nécessaire dans ce cas car for renvoie la valeur de la dernière top_expression de la liste d’expressions. La valeur d’une affectation est la valeur de son expression, donc la valeur de l’instruction for est ce qui a été affecté à new_tags.

Définition de la fonction

Si vous avez du code dans un modèle qui se répète, vous pouvez mettre ce code dans une fonction locale. Le mot clé « def » commence la définition. Il est suivi par le nom de la fonction, la liste des arguments, puis le code de la fonction. La définition de la fonction se termine par le mot-clé ``fed””.

Les arguments sont positionnels. Lorsqu’une fonction est appelée, les arguments fournis sont comparés de gauche à droite aux paramètres définis, la valeur de l’argument étant attribuée au paramètre. C’est une erreur de fournir plus d’arguments que de paramètres définis. Les paramètres peuvent avoir des valeurs par défaut, comme « a = 25 ». Si un argument n’est pas fourni pour ce paramètre, alors la valeur par défaut est utilisée, sinon le paramètre est défini par la chaîne vide.

L’instruction return peut être utilisée dans une fonction locale.

Une fonction doit être définie avant de pouvoir être utilisée.

Exemple : Ce modèle calcule une durée approximative en années, mois et jours à partir d’un nombre de jours. La fonction to_plural() met en forme les valeurs calculées. Notez que l’exemple utilise également l’opérateur &:

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')

Opérateurs relationnels

Les opérateurs relationnels renvoient '1' si la comparaison est vraie, sinon la chaîne vide (“”).

Il existe deux formes d’opérateurs relationnels : les comparaisons de chaînes de caractères et les comparaisons numériques.

Les comparaisons de chaînes de caractères effectuent une comparaison insensible à la casse en utilisant l’ordre lexical. Les opérateurs de comparaison de chaînes de caractères pris en charge sont ==, !=, <, <=, >, >=, in, et inlist. Pour l’opérateur in, le résultat de l’expression de gauche est interprété comme un motif d’expression régulière. L’opérateur in est True si la valeur de l’expression régulière de gauche correspond à la valeur de l’expression de droite. L’opérateur inlist est vrai si l’expression régulière de gauche correspond à l’un des éléments de la liste de droite, les éléments de la liste étant séparés par des virgules. Les correspondances sont insensibles à la casse.

Les opérateurs de comparaison numérique sont ==#, !=#, <#, <=#, >#, >=#. Les expressions de gauche et de droite doivent être évaluées par des valeurs numériques, à deux exceptions près : la chaîne de caractères « None » (champ indéfini) et la chaîne de caractères vide sont évaluées par la valeur zéro.

Exemples :

- program:field('series') == 'foo' renvoie '1' si la série du livre est “foo”, sinon ''.

- Program: 'f.o' in field('series') renvoie “1”` si la série du livre correspond à l’expression régulière f.o (par exemple, foo, Off Onyx, etc.), sinon ''.

- Program: 'science' inlist field('#genre') renvoie '1' si l’un des genres du livre correspond à l’expression régulière science, par exemple, Science, Histoire de la science, Science Fiction etc., sinon ''.

- Program: '^science$' inlist field('#genre') renvoie “1”` si l’un des genres du livre correspond exactement à l’expression régulière ^science$, par exemple, Science. Les genres Histoire des sciences et Science Fiction ne correspondent pas. S’il n’y a pas de correspondance, elle renvoie ''.

- Program: if field('series') != 'foo' then 'bar' else 'mumble' fi renvoie 'bar' si la série du livre n’est pas foo. Sinon, renvoie mumble.

- Program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi renvoie 'yes' si séries existe soit 'foo' soit '1632', sinon “no”`.

- program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi renvoie 'yes' si la série existe soit 'foo' ou '1632', sinon 'no'.

- program: if '11' > '2' then 'yes' else 'no' fi renvoie `”no”`` parce que l’opérateur > fait une comparaison lexicale.

- Program: if 11 ># 2 then 'yes' else 'no' fi renvoie 'yes' parce que l’opérateur ># effectue une comparaison numérique.

Fonctions supplémentaires disponibles

Les fonctions suivantes sont disponibles en plus de celles décrites dans Mode fonction unique.

Dans MPG, les fonctions décrites dans Mode Fonction Simple requièrent toutes un premier paramètre supplémentaire spécifiant la valeur sur laquelle opérer. Tous les paramètres sont des listes d’expressions (voir la grammaire ci-dessus).

add(x [, y]*) – renvoie la somme de ses arguments. Lance une exception si un argument n’est pas un nombre. Dans la plupart des cas, vous pouvez utiliser l’opérateur + à la place de cette fonction.

and(value [, value]*) – retourne la chaîne « 1 » si toutes les valeurs ne sont pas vides, sinon retourne la chaîne vide. Vous pouvez avoir autant de valeurs que vous le souhaitez. Dans la plupart des cas, vous pouvez utiliser l’opérateur && à la place de cette fonction. Une raison de ne pas remplacer and par && est que le court-circuitage peut modifier les résultats à cause d’effets secondaires. Par exemple, et(a='',b=5) fera toujours les deux affectations, alors que l’opérateur && ne fera pas la seconde.

assign(id, val) – affecte val à id, puis retourne val. id doit être un identifiant, pas une expression. Dans la plupart des cas, vous pouvez utiliser l’opérateur = à la place de cette fonction.

approximate_formats() – renvoie une liste, séparée par des virgules, des formats associés au livre. Il n’y a aucune garantie que la liste soit correcte, bien qu’elle le soit probablement. Cette fonction et d’autres fonctions à zéro paramètre peuvent être appelées en mode programme modèle (voir ci-dessous) en utilisant le modèle {:'approximate_formats()'}. Notez que les noms de formats résultants sont toujours en majuscules, comme dans EPUB. La fonction approximate_formats() est nettement plus rapide que les fonctions formats_... présentées ci-dessous.

author_links(val_separator, pair_separator) – renvoie une chaîne contenant une liste d’auteurs et les valeurs des liens de ces auteurs sous la forme

author1 val_separator author1_link pair_separator author2 val_separator author2_link etc.

Un auteur est séparé de sa valeur de lien par la chaîne val_separator sans espaces ajoutés. Les paires author:linkvalue sont séparées par la chaîne de caractères pair_separator, sans espaces ajoutés. C’est à vous de choisir des chaînes de séparation qui n’apparaissent pas dans les noms d’auteurs ou les liens. Un auteur est inclus même si le lien de l’auteur est vide.

author_sorts(val_separator) – renvoie une chaîne contenant une liste des valeurs de tri de l’auteur pour les auteurs du livre. Le tri est celui des métadonnées auteur (différente de l’author_sort dans les livres). La liste renvoyée a la forme author sort 1 val_separator author sort 2 etc. Les valeurs de tri auteur dans cette liste sont dans le même ordre que les auteurs du livre. Si vous voulez les espaces autour du val_separator incluez les alors dans la chaîne val_separator.

book_count(query, use_vl) – renvoie le nombre de livres trouvés en cherchant pour query. Si use_vl est 0 (zéro) alors les bibliothèques virtuelles sont ignorées. Cette fonction et son pendant book_values() sont particulièrement utiles dans les recherches de modèles, en supportant les recherches qui combinent les informations de plusieurs livres, comme la recherche de séries avec un seul livre. Elle ne peut pas être utilisée dans les colonnes composites, sauf si le paramètre « allow_template_database_functions_in_composites » a la valeur True. Elle ne peut être utilisée que dans l’interface graphique.

Par exemple, ce modèle de recherche utilise cette fonction et son pendant pour trouver toutes les séries ne comportant qu’un seul livre :

1. Définir un modèle enregistré (en utilisant Préférences → Avancé → Modèles de fonctions) nommé série_uniquement_un_livre (le nom est arbitraire). Le modèle est

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 première fois que le modèle s’exécute (le premier livre vérifié), il enregistre les résultats des recherches dans la base de données dans une variable de modèle « globale » appelée « vals ». Ces résultats sont utilisés pour vérifier les livres suivants sans avoir à refaire les recherches.

2. Utiliser le modèle enregistré dans une recherche de modèle

template:"program: series_only_one_book()#@#:n:1"

L’utilisation d’un modèle stocké au lieu de mettre le modèle dans la recherche élimine les problèmes causés par l’obligation d’échapper les guillemets dans les expressions de recherche.

book_values(column, query, sep, use_vl) – renvoie une liste des valeurs uniques contenues dans la colonne column (un nom de recherche), séparées par sep, dans les livres trouvés en recherchant query. Si use_vl est 0 (zéro), alors les bibliothèques virtuelles sont ignorées. Cette fonction et son pendant book_count() sont particulièrement utiles dans les recherches de modèles, en supportant les recherches qui combinent les informations de plusieurs livres, comme la recherche de séries avec un seul livre. Elle ne peut pas être utilisée dans des colonnes composites, sauf si le paramètre « allow_template_database_functions_in_composites » a la valeur True. Elle ne peut être utilisée que dans l’interface graphique.

booksize() – renvoie la valeur du champ “size” de calibre. Renvoie “” s’il n’y a pas de formats.

check_yes_no(field_name, is_undefined, is_false, is_true) – vérifie si la valeur du champ oui/non nommé par le nom de la référence externe field_name est l’une des valeurs spécifiées par les paramètres, renvoyant 'yes' si une correspondance est trouvée sinon renvoyant la chaîne vide. Définissez le paramètre is_undefined, is_false ou is_true à 1 (le nombre) pour vérifier cette condition, sinon définissez-le à 0. Exemple :

check_yes_no("#bool", 1, 0, 1) renvoie 'Yes' si le champ oui/non #bool est soit True soit non-défini (ni True ni False).

Plus d’un élément parmi «  is_undefined « , «  is_false «  ou «  is_true «  peut être mis à 1.

ceiling(x) – renvoie le plus petit entier supérieur ou égal à x. Envoie une exception si x n’est pas un nombre.

character(character_name) – renvoie le caractère désigné par character_name. Par exemple, character('newline') renvoie un caractère de nouvelle ligne ('\n'). Les noms de caractères pris en charge sont newline, return, tab, et backslash.

cmp(x, y, lt, eq, gt) – compare x et y après les avoir convertis en nombres. Renvoie lt si x <# y, eq si x ==# y, sinon gt. Cette fonction peut généralement être remplacée par l’un des opérateurs de comparaison numérique (==#, <#, >#, etc).

connected_device_name(storage_location_key) – si un périphérique est connecté alors retourne le nom du périphérique, sinon retourne la chaîne vide. Chaque emplacement de stockage sur un périphérique peut avoir un nom différent. Les noms storage_location_key sont 'main', 'carda' et 'cardb'. Cette fonction ne fonctionne que dans l’interface graphique.

connected_device_uuid(storage_location_key) – si un périphérique est connecté alors retourne l’uuid du périphérique (id unique), sinon retourne la chaîne vide. Chaque emplacement de stockage sur un périphérique a un uuid différent. Les noms storage_location_key sont 'main', 'carda' et 'cardb'. Cette fonction ne fonctionne que dans l’interface graphique.

current_library_name() – renvoie le dernier nom sur le chemin de la bibliothèque calibre actuelle.

current_library_path() – renvoie le chemin complet vers la bibliothèque calibre actuelle.

current_virtual_library_name() – retourne le nom de la bibliothèque virtuelle actuelle s’il y en a une, sinon la chaîne vide. La casse du nom de la bibliothèque est préservée. Exemple : « program:current_virtual_library_name() ». Cette fonction ne fonctionne que dans l’Interface Graphique.

date_arithmetic(date, calc_spec, fmt) – Calcule une nouvelle date à partir de date en utilisant calc_spec. Retourne la nouvelle date formatée selon l’option fmt : si elle n’est pas fournie, le résultat sera au format ISO. calc_spec est une chaîne de caractères formée par la concaténation de paires de vW (valueWhat) où v est un nombre éventuellement négatif et W est l’une des lettres suivantes :

- s: ajoute v secondes à date

- m: ajoute v minutes à date

- h: ajoute v heures à date

- d: ajoute v jours à date

- w: ajoute v semaines à date

- y: ajoute v années à date, où une année compte 365 jours.

Exemple : '1s3d-1m' ajoutera 1 seconde, ajoutera 3 jours, et soustraira 1 minute de date.

days_between(date1, date2) – renvoie le nombre de jours entre date1 et date2. Le nombre est positif si date1 est plus grand que date2, sinon négatif. Si date1 ou date2 ne sont pas des dates, la fonction renvoie une chaîne vide.

divide(x, y) – renvoie x / y. Lance une exception si x ou y ne sont pas des nombres. Cette fonction peut généralement être remplacée par l’opérateur /.

eval(string) – évalue la chaîne comme un programme, en passant les variables locales. Cela permet d’utiliser le processeur de modèles pour construire des résultats complexes à partir de variables locales. En Mode Modèle de Programme, comme les caractères { et } sont interprétés avant l’évaluation du modèle, vous devez utiliser [[] pour le caractère { et ]] pour le caractère }. Ils sont convertis automatiquement. Notez également que les préfixes et les suffixes (la syntaxe |prefix|suffix) ne peuvent pas être utilisés dans l’argument de cette fonction lors de l’utilisation de Mode Modèle de Programme.

extra_file_size(file_name) – retourne la taille en octets du fichier supplémentaire file_name dans le dossier data/ du livre s’il existe, sinon -1. Voir aussi les fonctions has_extra_files(), extra_file_names() et extra_file_modtime(). Cette fonction ne peut être utilisée que dans l’interface graphique.

extra_file_modtime(file_name, format_string) – retourne l’heure de modification du fichier supplémentaire file_name dans le dossier data/ du livre s’il existe, sinon -1. La date de modification est formatée selon format_spec (voir format_date() pour plus de détails). Si format_spec est une chaîne vide, elle retourne l’heure de modification sous la forme d’un nombre de secondes en virgule flottante depuis Epoch. Voir aussi les fonctions has_extra_files(), extra_file_names() et extra_file_size(). L’Epoch dépend du système d’exploitation. Cette fonction ne peut être utilisée que dans l’interface graphique.

extra_file_names(sep [, pattern]) – retourne une liste séparée de sep de fichiers supplémentaires dans le dossier data/ du livre. Si le paramètre optionnel pattern, une expression régulière, est fourni, alors la liste est filtrée sur les fichiers qui correspondent à pattern. La correspondance est insensible à la casse. Voir aussi les fonctions has_extra_files(), extra_file_modtime() et extra_file_size(). Cette fonction ne peut être utilisée que dans l’interface graphique.

field(lookup_name) – renvoie la valeur du champ de métadonnées avec le nom de recherche lookup_name.

field_exists(field_name) – vérifie si un champ (colonne) avec le nom de recherche field_name existe, retournant “1”` si c’est le cas et la chaîne vide si non.

finish_formatting(val, fmt, prefix, suffix) – applique le format, le préfixe et le suffixe à une valeur de la même manière que dans un modèle comme {series_index:05.2f| - |- }. Cette fonction est fournie pour faciliter la conversion des modèles complexes single-function- ou modèles template-program-mode en modèles MGP. Par exemple, le programme suivant produit la même sortie que le modèle ci-dessus

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Un autre exemple: pour le modèle {series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} utilisez:

program:
strcat(
re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
field('title')
)

first_matching_cmp(val, [ cmp, result, ]* else_result) – compare val < cmp en séquence, en retournant le résultat associé à la première comparaison qui réussit. Retourne else_result si aucune comparaison ne réussit. Exemple

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

renvoie "grand". Le même exemple avec une première valeur de 16 renvoie "géant".

first_non_empty(value [, value]*) – retourne la première valeur non vide. Si toutes les valeurs sont vides, alors la chaîne vide est retournée. Vous pouvez avoir autant de valeurs que vous voulez.

floor(x) – renvoie le plus grand entier inférieur ou égal à x. Envoie une exception si x n’est pas un nombre.

format_date(val, format_string) – formate la valeur, qui doit être une chaîne date, en utilisant format_string, retournant une chaîne. Les codes de formatage sont:

- d : le jour sous la forme d’un nombre sans zéro initial (1 à 31)

- dd : le jour sous la forme d’un nombre avec un zéro initial (1 à 31)

- ddd : le nom abrégé du jour localisé (par exemple de « Lun » à « Dim »).

- dddd : le nom long du jour localisé (par exemple de « lundi » à « dimanche »).

- M : le mois sous la forme d’un nombre sans zéro initial (1 à 12).

- MM : le mois sous la forme d’un nombre avec un zéro initial (1 à 12).

- MMM : le nom abrégé du mois localisé (par exemple de « Jan » à « Déc »).

- MMMM : le nom long du mois localisé (par exemple de « janvier » à « décembre »).

- yy : l’année sous la forme d’un nombre à deux chiffres (00 à 99).

- yyyy : l’année sous la forme d’un nombre à quatre chiffres.

- h : les heures sans le premier 0 (0 à 11 ou 0 à 23, selon am/pm)

- hh : les heures avec le premier 0 (00 à 11 ou 00 à 23, selon am/pm)

- m : les minutes sans le premier 0 (0 à 59)

- mm : les minutes avec le premier 0 (00 à 59)

- s : les secondes sans le premier 0 (0 à 59)

- ss : les secondes avec un 0 en tête (00 à 59)

- ap : utilise une horloge de 12 heures au lieu d’une horloge de 24 heures, avec “ap” remplacé par la chaîne localisée pour am ou pm.

- AP : utilise une horloge de 12 heures au lieu d’une horloge de 24 heures, avec “AP” remplacé par la chaîne localisée pour AM ou PM.

- iso : la date avec l’heure et le fuseau horaire. Ce doit être le seul format présent.

- to_number : convertit la date et l’heure en un nombre à virgule flottante (un timestamp)

- from_number : convertit un nombre à virgule flottante (un timestamp) en une date formatée iso. Si vous souhaitez un format de date différent, ajoutez la chaîne de formatage souhaitée après from_number et un deux-points (:). Exemple : from_number:MMM dd yyyy.

Vous pouvez obtenir des résultats inattendus si la date que vous formatez contient des noms de mois localisés, ce qui peut se produire si vous avez modifié les ajustements de format de date pour qu’ils contiennent MMMM. Dans ce cas, au lieu d’utiliser la fonction field() comme dans

format_date(field('pubdate'), 'yyyy')

utiliser la fonction raw_field() comme dans

format_date(raw_field('pubdate'), 'yyyy')

format_date_field(field_name, format_string) – formate la valeur du champ field_name, qui doit être le nom d’un champ date, standard ou personnalisé. Voir format_date() pour les codes de formatage. Cette fonction est beaucoup plus rapide que format_date et doit être utilisée lorsque vous formatez la valeur d’un champ (colonne). Elle ne peut pas être utilisée pour des dates calculées ou des dates dans des variables de type chaîne. Exemples :

format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')

formats_modtimes(date_format_string) – renvoie une liste séparée par des virgules d’éléments séparés par des deux points FMT:DATE représentant les heures de modification des formats d’un livre. Le paramètre date_format_string spécifie comment la date doit être formatée. Voir la fonction format_date() pour plus de détails. Vous pouvez utiliser la fonction select pour obtenir l’heure de modification d’un format spécifique. Notez que les noms de format sont toujours en majuscules, comme dans EPUB.

formats_paths() – retourne une liste d’éléments séparés par des virgules et des deux-points FMT:PATH donnant le chemin complet vers les formats d’un livre. Vous pouvez utiliser la fonction select pour obtenir le chemin d’accès à un format spécifique. Notez que les noms de format sont toujours en majuscules, comme dans EPUB.

formats_sizes() – renvoie une liste d’éléments FMT:SIZE séparés par des virgules, donnant les tailles en octets des formats d’un livre. Vous pouvez utiliser la fonction select pour obtenir la taille d’un format spécifique. Notez que les noms de format sont toujours en majuscules, comme dans EPUB.

fractional_part(x) – renvoie la valeur après la virgule. Par exemple, fractional_part(3.14) renvoie 0.14. Envoie une exception si x n’est pas un nombre.

get_link(field_name, field_value) – récupère le lien pour le champ field_name avec la valeur field_value. S’il n’y a pas de lien attaché, il renvoie “”. Exemple :

- Ce qui suit renvoie le lien attaché à l’étiquette Fiction.

get_link('tags', 'Fiction')

- Ce modèle crée une liste de liens pour tous les tags associés à un livre sous la forme valeur:lien, … :

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() – retourne 'Yes' si le livre a une couverture, sinon la chaîne vide.

has_extra_files([pattern]) – retourne le nombre de fichiers supplémentaires, sinon “” (la chaîne vide). Si le paramètre optionnel pattern (une expression régulière) est fourni, alors la liste est filtrée sur les fichiers qui correspondent à pattern avant que les fichiers ne soient comptés. La correspondance est insensible à la casse. Voir aussi les fonctions extra_file_names(), extra_file_size() et extra_file_modtime(). Cette fonction ne peut être utilisée que dans l’interface graphique.

identifier_in_list(val, id_name [, found_val, not_found_val]) – traite val comme une liste d’identifiants séparés par des virgules. Un identifiant a le format id_name:value. Le paramètre id_name est le texte de l’identifiant à rechercher, soit id_name soit id_name:regexp. Dans le premier cas, la recherche s’effectue s’il existe un identifiant correspondant à ce nom d’utilisateur. Dans le second cas, la recherche se fait si le id_name correspond à un identifiant et que l’expression rationnelle correspond à la valeur de l’identifiant. Si found_val et not_found_val sont fournis, alors s’il y a une correspondance, il retourne found_val, sinon il retourne not_found_val. Si found_val et not_found_val ne sont pas fournis, alors s’il y a une correspondance, alors la paire identifier:value est retournée, sinon la chaîne vide (“”).

is_marked() – vérifie si le livre est marqué dans calibre. S’il l’est, retourne la valeur de la marque, soit true' (minuscule), soit une liste de marques nommées, séparées par des virgules.. Si le livre n’est pas marqué, retourne " (chaîne vide). Cette fonction ne fonctionne que dans l’interface graphique.

language_codes(lang_strings) – retourne les codes de langue [https://www.loc.gov/standards/iso639-2/php/code_list.php] pour les noms de langue passés dans lang_strings. Les chaînes de caractères doivent être dans la langue de la locale courante. Lang_strings est une liste séparée par des virgules.

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – (Alias de in_list) Interprétant la valeur comme une liste d’éléments séparés par le separator, évalue le pattern par rapport à chaque valeur de la liste. Si le pattern correspond à une valeur, il renvoie found_val, sinon il renvoie not_found_val. Le pattern et la found_value peuvent être répétés autant de fois que vous le souhaitez, ce qui permet de renvoyer différentes valeurs en fonction de la recherche. Les motifs sont vérifiés dans l’ordre. La première correspondance est retournée. Alias : in_list(), list_contains().

list_count(value, separator) – interprète value comme une liste d’éléments séparés par separator, et retourne le nombre d’éléments dans la liste. Alias : count(), list_count()

list_count_matching(list, pattern, separator) – interprète list comme une liste d’éléments séparés par separator, en retournant le nombre d’éléments dans la liste qui correspondent à l’expression régulière pattern. Alias : list_count_matching(), count_matching()

list_difference(list1, list2, separator) – restitue une liste en supprimant de list1 tout élément trouvé dans list2 ``en utilisant une comparaison insensible à la casse. Les éléments dans ``list1 et list2 sont séparés par le séparateur, comme le sont les éléments dans la liste restituée.

list_equals(list1, sep1, list2, sep2, yes_val, no_val) –retourne yes_val si list1 et list2 contiennent les mêmes éléments, sinon renvoie no_val. Les éléments sont déterminés en éclatant chaque liste à l’aide du séparateur approprié (sep1 ou sep2). L’ordre des éléments dans la liste n’est pas pris en compte. La comparaison est insensible à la casse.

list_intersection(list1, list2, separator) – retourne une liste faite en retirant de list1 tout élément non trouvé dans list2, en utilisant une comparaison insensible à la casse. Les éléments de list1 et list2 sont séparés par un séparateur, comme le sont les éléments de la liste retournée.

list_join(with_separator, list1, separator1 [, list2, separator2]*) – retourne une liste faite en joignant les éléments des listes sources (list1 etc) en utilisant with_separator entre les éléments de la liste résultante. Les éléments de chaque liste[123...] source sont séparés par le séparateur[123...] associé. Une liste peut contenir zéro valeur. Il peut s’agir d’un champ comme publisher qui n’a qu’une seule valeur, c’est à dire une liste d’un seul élément. Les doublons sont supprimés en utilisant une comparaison insensible à la casse. Les éléments sont retournés dans l’ordre où ils apparaissent dans les listes sources. Si les éléments des listes ne diffèrent que par la casse des lettres, c’est la dernière qui est utilisée. Tous les séparateurs peuvent comporter plus d’un caractère.

Exemple:

program:
list_join('#@#', $authors, '&', $tags, ',')

Vous pouvez utiliser list_join sur les résultats des appels précédents à list_join comme suit

program:
a = list_join('#@#', $authors, '&', $tags, ',');
b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

Vous pouvez utiliser des expressions pour générer une liste. Par exemple, supposons que vous vouliez des éléments pour auteurs et #genre, mais avec le genre changé en mot « Genre : «  suivi de la première lettre du genre, c’est-à-dire que le genre « Fiction » devient « Genre : F ». Ce qui suit fera l’affaire

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

list_re(src_list, separator, include_re, opt_replace) – Construit une liste en séparant d’abord src_list en éléments en utilisant le caractère separator. Pour chaque élément de la liste, vérifiez s’il correspond à include_re. Si c’est le cas, ajoutez-le à la liste à retourner. Si opt_replace n’est pas une chaîne vide, il faut appliquer le remplacement avant d’ajouter l’élément à la liste retournée.

list_re_group(src_list, separator, include_re, search_re [, template_for_group]*) – Comme list_re sauf que les remplacements ne sont pas optionnels. Elle utilise re_group(item, search_re, template ...) pour effectuer les remplacements.

list_remove_duplicates(list, separator) – retourne une liste faite en supprimant les éléments en double dans list. Si les éléments ne diffèrent que par la casse, le dernier est retourné. Les éléments de list sont séparés par separator, comme le sont les éléments de la liste retournée.

list_sort(list, direction, separator) – retourne list triée en utilisant un tri lexical insensible à la casse. Si direction est zéro, list est triée de manière ascendante, sinon de manière descendante. Les éléments de la liste sont séparés par le separator, comme le sont les éléments de la liste retournée.

list_split(list_val, sep, id_prefix) – divise list_val en valeurs séparées en utilisant sep, puis affecte les valeurs à des variables locales nommées id_prefix_N où N est la position de la valeur dans la liste. Le premier élément a la position 0 (zéro). La fonction renvoie le dernier élément de la liste.

Exemple:

list_split('one:two:foo', ':', 'var')

est équivalent à

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

list_union(list1, list2, separator) – retourne une liste faite en fusionnant les éléments de list1 et list2, en supprimant les éléments en double en utilisant une comparaison insensible à la casse. Si les éléments diffèrent selon la casse, c’est celui de la « list1 » qui est utilisé. Les éléments de « list1 » et « list2 » sont séparés par un « separator », tout comme les éléments de la liste retournée. Alias : merge_lists(), list_union()

mod(x, y) – renvoie le floor du reste de x / y. Lance une exception si x ou y n’est pas un nombre.

multiply(x [, y]*) – renvoie le produit de ses arguments. Lance une exception si l’un des arguments n’est pas un nombre. Cette fonction peut généralement être remplacée par l’opérateur *.

not(valeur) – retourne la chaîne « 1 » si la valeur est vide, sinon retourne la chaîne vide. Cette fonction peut généralement être remplacée par l’opérateur unaire not (!).

ondevice() – retourne la chaîne Yes' si ondevice est défini, sinon retourne la chaîne vide.

or(value [, value]*) – retourne la chaîne '1' si une valeur n’est pas vide, sinon retourne la chaîne vide. Vous pouvez avoir autant de valeurs que vous le souhaitez. Cette fonction peut généralement être remplacée par l’opérateur ||. Une raison pour laquelle elle ne peut pas être remplacée est que le court-circuitage modifiera les résultats à cause d’effets secondaires.

print(a [, b]*) – imprime les arguments sur la sortie standard. A moins que vous ne lanciez calibre depuis la ligne de commande (calibre-debug -g), la sortie ira dans un trou noir. La fonction print retourne toujours son premier argument.

range(start, stop, step, limit) – renvoie une liste de nombres générés en bouclant sur la plage spécifiée par les paramètres start, stop, et step, avec une longueur maximale de limit. La première valeur produite est « start ». Les valeurs suivantes sont next_v = current_v + step. La boucle continue tant que next_v < stop si step` est positif, sinon tant que next_v > stop. Une liste vide est produite si start échoue le test : start >= stop si step est positif. Le paramètre limit définit la longueur maximale de la liste et a une valeur par défaut de 1000. Les paramètres start, step, et limit sont facultatifs. Appeler range() avec un argument spécifie stop. Deux arguments spécifient «  start «  et «  stop « . Trois arguments spécifient «  start « , «  stop «  et «  step « . Quatre arguments spécifient «  start « , «  stop « , «  step «  et «  limit « . Exemples

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(lookup_name [, optional_default]) – renvoie le champ de métadonnées nommé par lookup_name sans appliquer de formatage. Il évalue et renvoie le second argument facultatif optional_default si la valeur du champ est indéfinie (None).

raw_list(lookup_name, separator) – renvoie la liste de métadonnées nommée par lookup_name sans appliquer de formatage ou de tri, avec les éléments séparés par le séparateur.

re_group(value, pattern [, template_for_group]*) – renvoie une chaîne faite en appliquant le motif de l’expression régulière à value et en remplaçant chaque instance trouvée par la valeur renvoyée par le modèle correspondant. En Mode Programme Modèle, comme pour les fonctions template et eval, vous utilisez [[` pour { et ]] pour }.

L’exemple suivant recherche une série comportant plus d’un mot et met en majuscule le premier mot

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

round(x) – renvoie l’entier le plus proche de x. Envoie une exception si x n’est pas un nombre.

series_sort() – renvoie la valeur de tri des séries.

strcat(a [, b]*) – peut avoir n’importe quel nombre d’arguments. Renvoie une chaîne constituée par la concaténation de tous les arguments.

strcat_max(max, string1 [, prefix2, string2]*) – Renvoie une chaîne formée par la concaténation des arguments. La valeur retournée est initialisée à string1. Les chaînes formées à partir des paires prefix, string sont ajoutées à la fin de la valeur tant que la longueur de la chaîne résultante est inférieure à max. Les préfixes peuvent être vides. Retourne string1 même si string1 est plus long que max. Vous pouvez passer autant de paires prefix, string que vous le souhaitez.

strcmp(x, y, lt, eq, gt) – effectue une comparaison lexicale insensible à la casse de x et y. Elle retourne lt si x < y, eq si x == y, sinon gt. Cette fonction peut souvent être remplacée par l’un des opérateurs de comparaison lexicale (==, >, <, etc.).

strcmpcase(x, y, lt, eq, gt) – effectue une comparaison sensible à la casse de x et y en tant que chaînes de caractères. Retourne lt si x < y. Retourne eq si x == y. Sinon, retourne gt.

Note : Ce n’est PAS le comportement par défaut utilisé par calibre, par exemple, dans les opérateurs de comparaison lexicale (==, >, <, etc.). Cette fonction pourrait provoquer des résultats inattendus, utilisez de préférence strcmp() lorsque cela est possible.

strlen( value) – Retourne la longueur de la chaîne value.

substr(str, start, end) – renvoie les caractères de début jusqu’à fin de str. Le premier caractère dans str est le caractère zéro. Si end est négatif, cela indique que beaucoup de caractères sont comptés à partir de la droite. Si end est zéro, cela indique le dernier caractère. Par exemple, substr('12345', 1, 0) renvoie '2345', et substr('12345', 1, -1) renvoie '234'.

substract(x, y) – renvoie x / y. Lance une exception si x ou y ne sont pas des nombres. Cette fonction peut généralement être remplacée par l’opérateur -.

switch_if([test_expression, value_expression,]+ else_expression) – pour chaque paire test_expression, value_expression, vérifie si test_expression est vraie (non vide) et si c’est le cas retourne le résultat de value_expression. Si aucune expression_test n’est vraie, alors le résultat de expression_sel est retourné. Vous pouvez avoir autant de paires expression_test, expression_valeur que vous le souhaitez.

today() – retourne une chaîne date+heure pour aujourd’hui (now). Cette valeur est conçue pour être utilisée dans format_date ou days_between, mais peut être manipulée comme toute autre chaîne. La date est au format `ISO `_ date/heure.

template(x) – évalue x comme un modèle. L’évaluation est faite dans son propre contexte, ce qui signifie que les variables ne sont pas partagées entre l’appelant et l’évaluation du modèle.

to_hex(val) – renvoie la chaîne val encodée en hexadécimal. Ceci est utile lors de la construction d’URLs calibre.

urls_from_identifiers(identifiers, sort_results) – donne une liste d’identifiants séparée par des virgules, où un identifiant est une paire de valeurs séparées par des deux points (id_name:id_value), renvoie une liste d’URL HTML séparées par des virgules, générées à partir des identifiants. La liste n’est pas triée si sort_results vaut 0 (caractère ou nombre), sinon elle est triée par ordre alphabétique du nom de l’identifiant. Les URL sont générées de la même manière que la colonne d’identifiants intégrée lorsqu’elle est affichée dans Détails du livre.

Programmes plus complexes dans les expressions modèles - Mode Programme de Modèles

Le Mode Programme de Modèle (MPM) est un mélange du Mode de Programme Général et du Mode Fonction Unique. Le MPM diffère du Mode Fonction Unique en ce qu’il permet d’écrire des expressions de modèle qui font référence à d’autres champs de métadonnées, utilisent des fonctions imbriquées, modifient des variables et font de l’arithmétique. Il diffère du Mode Programme Général en ce que le modèle est contenu entre les caractères {` et } et ne commence pas par le mot program:. La partie programme du modèle est une liste d’expressions du Mode Programme Général.

Exemple : supposons que vous vouliez qu’un modèle affiche la série d’un livre s’il en a une, sinon la valeur d’un champ personnalisé #genre. Vous ne pouvez pas faire cela dans le mode Mode Fonction Unique car vous ne pouvez pas faire référence à un autre champ de métadonnées dans une expression de modèle. En MPM, vous le pouvez, comme le montre l’expression suivante

{#series:'ifempty($, field('#genre'))'}

L’exemple montre plusieurs choses :

- MPP est utilisé si l’expression commence par :' et se termine par '}. Tout le reste est supposé être en Mode Fonction Unique.

- la variable $ représente le champ nommé dans le modèle : l’expression sur laquelle on opère, #series dans ce cas.

- les fonctions doivent être données avec tous leurs arguments. Il n’y a pas de valeur par défaut. Par exemple, les fonctions intégrées standard doivent recevoir un paramètre initial supplémentaire indiquant le champ source.

- les espaces sont ignorés et peuvent être utlisés n’importe où dans l’expression.

- les chaînes constantes sont entre guillemets, soit ' ou ".

Toutes les fonctions listées dans les rubriques Mode Fonction Unique et Mode Programme Général peuvent être utilisées dans le MPM.

En MPM, l’utilisation des caractères {` et } dans les chaînes de caractères peut conduire à des erreurs ou des résultats inattendus parce qu’ils confondent le processeur de modèle. Il essaie de les traiter comme des limites d’expression de modèle et non comme des caractères. Dans certains cas, mais pas dans tous, vous pouvez remplacer un { par [[ et un } par ]]. En général, si votre programme contient des caractères { et }, vous devriez utiliser le Mode Programme Général.

Comme pour le Mode Général de Programme, pour les fonctions documentées sous Mode Fonction Unique, vous devez fournir la valeur sur laquelle la fonction doit agir comme premier paramètre en plus des paramètres documentés. En MPM, vous pouvez utiliser $ pour accéder à la valeur spécifiée par le lookup name de l’expression modèle.

Mode Programmation Python

Mode Programmation Python (MPP) vous permet d’écrire des modèles en utilisant le langage python natif et l’API de calibre [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. L’API de la base de données sera la plus utile ; une discussion plus approfondie dépasse le cadre de ce manuel. Les modèles MPP sont plus rapides et peuvent faire des opérations plus compliquées, mais vous devez savoir comment écrire du code en python en utilisant l’API calibre.

Un modèle de MPP commence par :

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'

Vous pouvez ajouter le texte ci-dessus à votre modèle en utilisant le menu contextuel, généralement accessible par un clic droit. Les commentaires ne sont pas significatifs et peuvent être supprimés. Vous devez utiliser l’indentation python.

L’objet context prend en charge str(context) qui renvoie une chaîne de caractères du contenu du contexte, et context.attributes qui renvoie une liste des noms d’attributs dans le contexte.

L’attribut context.funcs permet d’appeler les fonctions modèles intégrés et utilisateurs, ainsi que les modèles stockés MPG/Python, afin que vous puissiez les exécuter directement dans votre code. Les fonctions sont récupérées à l’aide de leur nom. Si le nom entre en conflit avec un mot clé Python, ajoutez un trait de underscore à la fin du nom. Exemples :

context.funcs.list_re_group()
context.funcs.assert_()

Voici un exemple de modèle MPP qui produit une liste de tous les auteurs d’une série. La liste est stockée dans une Colonne construite à partir d’autres colonnes, se comporte comme des étiquettes. Elle est affichée dans Détails du livre et l’option sur des lignes séparées est cochée (dans Préférences → Apparence et présentation → Détails du livre). Cette option exige que la liste soit séparée par des virgules. Pour satisfaire à cette exigence, le modèle convertit les virgules dans les noms d’auteurs en points-virgules puis construit une liste d’auteurs séparés par des virgules. Les auteurs sont ensuite triés, c’est pourquoi le modèle utilise 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 sortie dans Détails du livre ressemble à ceci :

Modèles stockés

Les modes Mode Général de Programmation et Mode Programmation Python permettent d’enregistrer des modèles et d’appeler ces modèles à partir d’un autre modèle, de la même manière que l’on appelle des fonctions stockées. Vous enregistrez les modèles en utilisant Préférences → Avancées → Modèles de fonctions. Plus d’informations sont fournies dans cette boîte de dialogue. Vous appelez un modèle de la même manière que vous appelez une fonction, en passant des arguments positionnels si vous le souhaitez. Un argument peut être n’importe quelle expression. Exemples d’appel d’un modèle, en supposant que le modèle stocké est nommé foo

- foo() – appelle le modèle en ne passant aucun argument.

- foo(a, b) appelle le modèle en passant les valeurs des deux variables a et b.

- foo(if field('series') then field('series_index') else 0 fi) – si le livre a series alors passez le series_index, sinon passez la valeur 0.

En MGP, vous récupérez les arguments passés dans l’appel au modèle stocké en utilisant la fonction arguments. Cette fonction déclare et initialise des variables locales, en fait des paramètres. Les variables sont positionnelles ; elles obtiennent la valeur du paramètre donné dans l’appel à la même position. Si le paramètre correspondant n’est pas fourni dans l’appel, alors arguments attribue à cette variable la valeur par défaut fournie. S’il n’y a pas de valeur par défaut, la variable prend la valeur de la chaîne vide. Par exemple, la fonction arguments suivante déclare 2 variables, key, alternate :

arguments(key, alternate='series')

Par exemple, en supposant encore une fois que le modèle stocké est nommé foo :

- foo('#myseries') – l’argument key se voit attribuer la valeur myseries' et l’argument alternate se voit attribuer la valeur par défaut 'series'.

- foo('series', '#genre') la variable key prend la valeur 'series' et la variable alternate prend la valeur '#genre'.

- foo() – la variable key se voit attribuer la chaîne vide et la variable alternate se voit attribuer la valeur 'series'.

En MPP, les arguments sont passés dans le paramètre arguments, qui est une liste de chaînes de caractères. Il n’y a aucun moyen de spécifier des valeurs par défaut. Vous devez vérifier la longueur de la liste arguments pour être sûr que le nombre d’arguments est celui que vous attendez.

Une façon simple de tester les modèles stockés est d’utiliser le dialogue « Testeur de modèle ». Pour faciliter l’accès, donnez-lui un raccourci clavier dans Préférences → Avancé → Raccourcis clavier → Tester le modèle`. Donner un raccourci au dialogue Modèles stockés aidera à passer plus rapidement du testeur à l’édition du code source du modèle stocké.

Fournir des informations supplémentaires aux modèles

Un développeur peut choisir de transmettre des informations supplémentaires au processeur du modèle, telles que des métadonnées de livre spécifiques à l’application ou des informations sur ce qui est demandé au processeur. Un modèle peut accéder à ces informations et les utiliser pendant l’évaluation.

Développeur : comment transmettre des informations supplémentaires

L’information supplémentaire est un dictionnaire Python contenant des paires nom_variable : valeur_variable où les valeurs doivent être des chaînes. Le modèle peut accéder au dictionnaire, en créant des variables locales nommées nom_de_variable contenant la valeur variable_valeur. L’utilisateur ne peut pas changer le nom, il est donc préférable d’utiliser des noms qui n’entreront pas en collision avec d’autres variables locales du modèle, par exemple en préfixant le nom par un trait de soulignement.

Ce dictionnaire est passé au processeur de modèle (le formatter) en utilisant le paramètre nommé global_vars=votre_dict. La signature complète de la méthode est :

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

Rédacteur de modèles : comment accéder aux informations complémentaires

Vous accédez aux informations supplémentaires (le dictionnaire globals) dans un modèle en utilisant la fonction de modèle :

globals(id[=expression] [, id[=expression]]*)

où id est un nom de variable légale quelconque. Cette fonction vérifie si les informations supplémentaires fournies par le développeur contiennent le nom. Si c’est le cas, la fonction attribue la valeur fournie à une variable locale modèle avec ce nom. Si le nom ne figure pas dans les informations supplémentaires et si une « expression » est fournie, cette « expression » est évaluée et le résultat est attribué à la variable locale. Si ni une valeur ni une expression n’est fournie, la fonction attribue la chaîne vide ('') à la variable locale.

Un modèle peut définir une valeur dans le dictionnaire globals en utilisant la fonction de modèle:

set_globals(id[=expression] [, id[=expression]]*)

Cette fonction définit la paire clé:valeur du dictionnaire globals id:value où value est la valeur de la variable locale du modèle id. Si cette variable locale n’existe pas, alors valeur est fixée au résultat de l’évaluation de expression.

Notes sur la différence entre les modes

Les trois modes de programmation, Mode Fonction Simple (MFS), Mode Modèle de Programmation (MMP), et Mode Général de Programmation (MGP), fonctionnent différemment. Le MFS est destiné à être “simple” et cache donc beaucoup de bits de langage de programmation.

Différences :

- Dans MFS, la valeur de la colonne est toujours passée comme premier argument “invisible” à une fonction incluse dans le modèle.

- Le MFS ne supporte pas la différence entre les variables et les chaînes de caractères ; toutes les valeurs sont des chaînes de caractères.

- Le modèle SFM suivant renvoie soit le nom de la série, soit la chaîne « pas de série »

{series:ifempty(no series)}

Le modèle équivalent dans MPM est

{series:'ifempty($, 'no series')'}

Le modèle équivalent dans MGP est

program: ifempty(field('series'), 'no series')

Le premier argument de ifempty est la valeur du champ série. Le second argument est la chaîne no series. Dans SFM, le premier argument, la valeur du champ, est automatiquement passé (l’argument invisible).

- Plusieurs fonctions du modèle, par exemple booksize() et current_library_name(), ne prennent aucun argument. En raison de l“« argument invisible », vous ne pouvez pas utiliser ces fonctions dans SFM.

- Les fonctions imbriquées, où une fonction appelle une autre fonction pour calculer un argument, ne peuvent pas être utilisées dans SFM. Par exemple, ce modèle, destiné à renvoyer les 5 premiers caractères de la valeur de la série en majuscules, ne fonctionnera pas dans SFM

{series:uppercase(substr(0,5))}

- MPM et MPG soutiennent des fonctions imbriquées. Le modèle ci-dessus dans MPM serait

{series:'uppercase(substr($, 0,5))'}

Dans MPG, ce serait

program: uppercase(substr(field('series'), 0,5))

- Comme indiqué dans la section Mode de Programmation des Modèles ci-dessus, l’utilisation des caractères { et } dans les chaînes de caractères MPM peut conduire à des erreurs ou à des résultats inattendus car ils perturbent le processeur de modèles. Il essaie de les traiter comme des limites de modèles et non comme des caractères. Dans certains cas, mais pas dans tous, vous pouvez remplacer un { par [[ et un } par ]]`. En général, si votre programme contient des caractères ``{ et }, vous devriez utiliser le Mode Général de Programmation.

Fonctions des modèles Python définis par l’utilisateur

Vous pouvez ajouter vos propres fonctions Python au processeur de modèles. Ces fonctions peuvent être utilisées dans l’un des trois modes de programmation du modèle. Les fonctions sont ajoutées en allant dans Préférences  →  Avancé  →  Fonctions de modèle. Les instructions sont affichées dans cette boite de dialogue.

Notes spéciales pour la sauvergarde/l’envoi des modèles

Un traitement spécial est appliqué quand un modèle est utilisé dans un modèle Enregistrer sur le disque ou Envoyer au périphérique. Les valeurs des champs sont nettoyées, les caractères qui sont propres aux systèmes de fichiers sont remplacées par des traits de soulignement, y compris les barres obliques. Cela signifie que le texte d’un champ ne peut pas être utilisé pour créer des dossiers. Toutefois, les barres obliques ne sont pas modifiés dans des chaînes préfixe ou suffixe. Vous pouvez donc insérer des barres obliques qui et provoquer ainsi la création de dossiers. Ainsi, vous pouvez créer une structure de dossier avec des profondeurs variables.

Par exemple, supposons que nous voulons que la structure des dossiers soit series/series_index - titre, en prévoyant que si la série n’existe pas, alors le titre devrait être dans le dossier racine (le premier dossier, pas le sous-dossier). Le modèle pour faire cela est :

{series:||/}{series_index:|| - }{title}

La barre oblique et le trait d’union apparaissent uniquement si la série n’est pas vide.

La fonction de recherche nous permet de faire le traitement encore plus poussé. Par exemple, supposons que si un livre est dans une série, alors nous voulons que la structure soit la suivante : series/series index - title.fmt.. Si le livre n’est pas dans une série, alors nous voulons que la structure des dossiers soit genre/author_sort/title.fmt. Si le livre n’a pas de genre, alors nous voulons utiliser “Inconnu”. Nous voulons deux chemins de répertoire totalement différents en fonction de la valeur de la série.

Pour accomplir cela, nous :

1. Créer un champ composite (donner-lui le nom de recherche #aa) contenant {series}/{series_index} - {titre}. Si la série n’est pas vide, alors ce modèle produira series/series_index - titre.

2. Créons une champ composite (appelons-le #bb) contenant {#genre:ifempty(Unknown)}/{author_sort}/{title}. Ce modèle produit genre/author_sort/title, où un genre vide est remplacé par Inconnu.

3. Paramétrez le modèle de sauvegarde à {series:lookup(.,#aa,#bb)}. Ce modèle choisit le champ composite #aa si série n’est pas vide, et le champ composite #bb si série est vide. Nous avons dès lors deux chemins de sauvegarde complètement différents, dépendant du fait que series soit vide ou pas.

Astuces

- Utilisez le Testeur de Modèles pour tester les modèles. Ajoutez le testeur au menu contextuel des livres de la bibliothèque et/ou donnez-lui un raccourci clavier.

- Les modèles peuvent utiliser d’autres modèles en faisant référence à des colonnes composites construites avec le modèle souhaité. Vous pouvez également utiliser des Modèles Stockés comme alternative.

- Dans un tableau de connexions, vous pouvez paramétrer un champ à vide (ou quelque soit sont équivalent à vide) en utilisant le modèle spécial {}. Ce modèle sera toujours évalué à une chaîne vide.

- La technique indiquée ci-dessus pour montrer les nombre même s’ils ont une valeur zéro fonctionne avec le champ standard series_index.

Référence de la fonction

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(valeur [, valeur]*)
- not(value)
- or(valeur [, valeur]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)
- days_between(date1, date2)
- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, chaine_format)

- format_date_field(field_name, format_string)

- format_number(v, template)
- human_readable(v)
- rating_to_stars(value, use_half_stars)

- urls_from_identifiers(identifiers, sort_results)

Get values from metadata

- annotation_count()
- approximate_formats()
- author_links(val_separator, pair_separator)
- author_sorts(val_separator)
- booksize()
- connected_device_name(storage_location)
- connected_device_uuid(storage_location)
- current_library_name()
- current_library_path()
- current_virtual_library_name()
- field(lookup_name)
- formats_modtimes(date_format)
- formats_paths()
- formats_sizes()
- has_cover()
- is_marked()
- language_codes(lang_strings)
- language_strings(lang_codes, localize)
- ondevice()

- raw_field(lookup_name [, optional_default]))

- raw_field(lookup_name, separator)
- series_sort()
- user_categories()
- virtual_libraries()

If-then-else

- check_yes_no(field_name, is_undefined, is_false, is_true)

- contains(val, pattern, texte si correspondance, texte si pas de correspondance)

- field_exists(field_name)
- ifempty(val, texte si vide)
- test(val, text if not empty, text if empty)

Iterating over values

- first_non_empty(value [, value]*)
- lookup(val, [pattern, field,]+ else_field)
- switch(val, [pattern, value,]+ else_value)

- switch_if([test_expression, value_expression,]+ else_expression)

List lookup

- identifier_in_list(val, id_name [, found_val, not_found_val])

- in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

- list_item(val, index, separator)

- select(val, key)

- str_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(separator)

- list_count_matching(list, pattern, separator)

- list_difference(list1, list2, separator)

- list_equals(list1, sep1, list2, sep2, yes_val, no_val)

- list_intersection(list1, list2, separator)

- list_join(with_separator, list1, separator1 [, list2, separator2]*)

- list_re(src_list, separator, include_re, opt_replace)

- list_re_group(src_list, separator, include_re, search_re [, group_template]+)

- list_remove_duplicates(list, separator)
- list_sort(liste, direction, separator)
- list_split(list_val, sep, id_prefix)
- list_union(list1, list2, separator)
- range(start, stop, step, limit)
- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, séparator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(modèle)

- template(x)

Relational

- cmp(x, y, lt, eq, gt)

- first_matching_cmp(val, cmp1, result1,]+, else_result)

- strcmp(x, y, lt, eq, gt)

- strcmpcase(x, y, lt, eq, gt)

String case changes

- capitalize(val)
- lowercase(val)
- titlecase(val)
- uppercase(val)

String manipulation

- character(character_name)

- re(val, pattern, replacement)

- re_group(val, pattern [, template_for_group]*)

- shorten(val, left chars, middle text, right chars)

- strcat(a [, b]*)

- strcat_max(max, string1 [, prefix2, string2]*)

- strlen(a)
- substr(ch, start, end)
- swap_around_articles(separator)
- swap_around_comma(val)
- to_hex(val)
- transliterate(a)

Template database functions

- book_count(query, use_vl)

- book_values(column, query, sep, use_vl)

- extra_file_modtime(file_name, format_string)

- extra_file_names(sep [, pattern])
- extra_file_size(file_name)
- get_link(field_name, field_value)

- get_note(field_name, field_value, plain_text)

- has_extra_files([pattern])

- has_note(field_name, field_value)

other

- set_globals(id[=expression] [, id[=expression]]*)

API of the Metadata objects

- Metadata

- STANDARD_METADATA_FIELDS

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »
- Le langage de modèle 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(valeur [, valeur]*)

- not(value)

- or(valeur [, valeur]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)

- days_between(date1, date2)

- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, chaine_format)

- format_date_field(field_name, format_string)

- format_number(v, template)

- human_readable(v)

- rating_to_stars(value, use_half_stars)

- urls_from_identifiers(identifiers, sort_results)

Get values from metadata

- annotation_count()

- approximate_formats()

- author_links(val_separator, pair_separator)

- author_sorts(val_separator)

- booksize()

- connected_device_name(storage_location)

- connected_device_uuid(storage_location)

- current_library_name()

- current_library_path()

- current_virtual_library_name()

- field(lookup_name)

- formats_modtimes(date_format)

- formats_paths()

- formats_sizes()

- has_cover()

- is_marked()

- language_codes(lang_strings)

- language_strings(lang_codes, localize)

- ondevice()

- raw_field(lookup_name [, optional_default]))

- raw_field(lookup_name, separator)

- series_sort()

- user_categories()

- virtual_libraries()

If-then-else

- check_yes_no(field_name, is_undefined, is_false, is_true)

- contains(val, pattern, texte si correspondance, texte si pas de correspondance)

- field_exists(field_name)

- ifempty(val, texte si vide)

- test(val, text if not empty, text if empty)

Iterating over values

- first_non_empty(value [, value]*)

- lookup(val, [pattern, field,]+ else_field)

- switch(val, [pattern, value,]+ else_value)

- switch_if([test_expression, value_expression,]+ else_expression)

List lookup

- identifier_in_list(val, id_name [, found_val, not_found_val])

- in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

- list_item(val, index, separator)

- select(val, key)

- str_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(separator)

- list_count_matching(list, pattern, separator)

- list_difference(list1, list2, separator)

- list_equals(list1, sep1, list2, sep2, yes_val, no_val)

- list_intersection(list1, list2, separator)

- list_join(with_separator, list1, separator1 [, list2, separator2]*)

- list_re(src_list, separator, include_re, opt_replace)

- list_re_group(src_list, separator, include_re, search_re [, group_template]+)

- list_remove_duplicates(list, separator)

- list_sort(liste, direction, separator)

- list_split(list_val, sep, id_prefix)

- list_union(list1, list2, separator)

- range(start, stop, step, limit)

- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, séparator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(modèle)

- template(x)

Relational

- cmp(x, y, lt, eq, gt)

- first_matching_cmp(val, cmp1, result1,]+, else_result)

- strcmp(x, y, lt, eq, gt)

- strcmpcase(x, y, lt, eq, gt)

String case changes

- capitalize(val)

- lowercase(val)

- titlecase(val)

- uppercase(val)

String manipulation

- character(character_name)

- re(val, pattern, replacement)

- re_group(val, pattern [, template_for_group]*)

- shorten(val, left chars, middle text, right chars)

- strcat(a [, b]*)

- strcat_max(max, string1 [, prefix2, string2]*)

- strlen(a)

- substr(ch, start, end)

- swap_around_articles(separator)

- swap_around_comma(val)

- to_hex(val)

- transliterate(a)

Template database functions

- book_count(query, use_vl)

- book_values(column, query, sep, use_vl)

- extra_file_modtime(file_name, format_string)

- extra_file_names(sep [, pattern])

- extra_file_size(file_name)

- get_link(field_name, field_value)

- get_note(field_name, field_value, plain_text)

- has_extra_files([pattern])

- has_note(field_name, field_value)

other

- set_globals(id[=expression] [, id[=expression]]*)

API of the Metadata objects

Arithmetic

add(x [, y]*)

class calibre.utils.formatter_functions.BuiltinAdd[source]

add(x [, y]*) – retourne la somme de ses arguments. Déclenche une exception si un argument n’est pas un nombre. Cette fonction peut souvent être remplacée par l’opérateur +.

ceiling(x)

class calibre.utils.formatter_functions.BuiltinCeiling[source]

ceiling(x) – renvoie le plus petit entier supérieur ou égal à x. Envoie une exception si x n’est pas un nombre.

divide(x, y)

class calibre.utils.formatter_functions.BuiltinDivide[source]

divide(x, y) – renvoie x / y. Déclenche une exception si x ou y ne sont pas des nombres. Cette fonction peut souvent être remplacée par l’opérateur /.

floor(x)

class calibre.utils.formatter_functions.BuiltinFloor[source]

floor(x) – renvoie le plus grand entier inférieur ou égal à x. Envoie une exception si x n’est pas un nombre.

fractional_part(x)

class calibre.utils.formatter_functions.BuiltinFractionalPart[source]

fractional_part(x) – renvoie la valeur après la virgule. Par exemple, fractional_part(3.14) renvoie 0.14. Envoie une exception si x n’est pas un nombre.

mod(x)

class calibre.utils.formatter_functions.BuiltinMod[source]

mod(x) – renvoie le solde (reste de x / y). Déclenche une exception si x ou y n’est pas un nombre.

multiply(x [, y]*)

class calibre.utils.formatter_functions.BuiltinMultiply[source]

multiply(x [, y]*) – renvoie le produit de ses arguments. Déclenche une exception si l’un des arguments n’est pas un nombre. Cette fonction peut souvent être remplacée par l’opérateur *.

round(x)

class calibre.utils.formatter_functions.BuiltinRound[source]

round(x) – renvoie l’entier le plus proche de x. Envoie une exception si x n’est pas un nombre.

subtract(x, y)

class calibre.utils.formatter_functions.BuiltinSubtract[source]

subtract(x, y) – renvoie x - y. Déclenche une exception si x ou y ne sont pas des nombres. Cette fonction peut souvent être remplacée par l’opérateur -.

Boolean

and(valeur [, valeur]*)

class calibre.utils.formatter_functions.BuiltinAnd[source]

and(valeur [, valeur]*) – renvoie la chaîne « 1 » si toutes les valeurs ne sont pas vides, sinon renvoie la chaîne vide. Cette fonction fonctionne bien avec test ou first_non_empty. Vous pouvez avoir autant de valeurs que vous le souhaitez. Dans de nombreux cas, l’opérateur && peut remplacer cette fonction.

not(value)

class calibre.utils.formatter_functions.BuiltinNot[source]

not(value) – retourne la chaîne « 1 » si la valeur est vide, sinon retourne la chaîne vide. Cette fonction fonctionne bien avec test ou first_non_empty. Dans de nombreux cas, l’opérateur ! peut remplacer cette fonction.

or(valeur [, valeur]*)

class calibre.utils.formatter_functions.BuiltinOr[source]

or(valeur [, valeur]*) – retourne la chaîne « 1 » si une valeur n’est pas vide, sinon retourne la chaîne vide. Cette fonction fonctionne bien avec test ou first_non_empty. Vous pouvez avoir autant de valeurs que vous le souhaitez. Dans de nombreux cas, l’opérateur || peut remplacer cette fonction.

Date functions

date_arithmetic(date, calc_spec, fmt)

class calibre.utils.formatter_functions.BuiltinDateArithmetic[source]

date_arithmetic(date, calc_spec, fmt) – Calcule une nouvelle date à partir de “date” en utilisant “calc_spec”. Retourne la nouvelle date formatée selon l’option “fmt” : si elle n’est pas fournie, le résultat sera au format iso. calc_spec est une chaîne de caractères formée par la concaténation de paires de “vW” (valueWhat) où “v” est un nombre éventuellement négatif et W est l’une des lettres suivantes : s : ajoute “v” secondes à “date” m : ajoute “v” minutes à “date” h : ajoute “v” heures à “date” d : ajoute “v” jours à “date” w : ajoute “v” semaines à “date” y : ajoute “v” années à “date”, où une année est de 365 jours. Exemple : “1s3d-1m” ajoutera 1 seconde, ajoutera 3 jours et soustraira 1 minute à la “date”.

days_between(date1, date2)

class calibre.utils.formatter_functions.BuiltinDaysBetween[source]

days_between(date1, date2) – renvoie le nombre de jours entre date1 et date2. Le nombre est positif si date1 est plus grand que date2, sinon négatif. Si date1 ou date2 ne sont pas des dates, la fonction renvoie une chaîne vide.

today()

class calibre.utils.formatter_functions.BuiltinToday[source]

today() – renvoie une chaîne date pour aujourd’hui. Cette valeur est conçue pour être utilisée avec format_date ou days_between, mais peut être manipulée comme toute autre chaîne. La date est au format ISO.

Formatting values

finish_formatting(val, fmt, prefix, suffix)

class calibre.utils.formatter_functions.BuiltinFinishFormatting[source]

finish_formatting(val, fmt, prefix, suffix) – applique le format, le préfixe et le suffixe à une valeur de la même manière que cela est effectué dans un modèle comme {series_index:05.2f| - |- }. Par exemple, le programme suivant produit le même résultat que le modèle ci-dessus : programme : finish_formatting(field(« series_index »), « 05.2f », «  - « , «  - « )

format_date(val, chaine_format)

class calibre.utils.formatter_functions.BuiltinFormatDate[source]

format_date(val, chaine_format) – formate la valeur, qui doit être une date, en utilisant la chaine_format, retournant une chaine. Les codes de formatage sont : d : le jour comme nombre sans zéro (1 à 31) dd :le jour comme nombre avec zéro (01 à 31) ddd : le nom du jour localisé et abrégé (p. ex. « Lun » to « Dim »). dddd : le nom du jour localisé en entier (p. ex. « Lundi » to « Dimanche »). M : le mois comme nombre sans zéro (1 à 12). MM : le mois comme nombre avec zéro (01 à 12) MMM : le nom du mois localisé et abrégé (p. ex. « Jan » to « Dec »). MMMM : le nom du mois localisé en entier (p. ex. « Janvier » to « Décembre »). yy : l’année comme nombre à 2 chiffres (00 à 99). yyyy : l’année comme nombre à 4 chiffres. h : les heures sans zéro (0 à 11 où 0 à 23, en fonction de am/pm) hh : les heures avec zéro (00 à 11 où 00 à 23, en fonction de am/pm) m : les minutes sans zéro (0 à 59) mm : les minutes avec zéro(00 à 59) s : les secondes sans zéro (0 à 59) ss : les secondes avec zéro (00 à 59) ap : utiliser une horloge à 12-heures au lieu de 24-heures, avec « ap » remplacé par la chaine localisé pour am ou pm AP : utiliser une horloge à 12-heures au lieu de 24-heures, avec « AP » remplacé par la chaine localisé pour AM ou PM iso : la date avec le temps et le fuseau horaire. Doit être le seul format présent to_number : la date sous forme de nombre à virgule flottante from_number[:fmt] : formate l’horodatage en utilisant fmt si présent sinon iso

format_date_field(field_name, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDateField[source]

format_date_field(field_name, format_string) – formate la valeur du champ “field_name”, qui doit être le nom d’un champ date, standard ou personnalisé. Voir “format_date()” pour les codes de formatage. Cette fonction est beaucoup plus rapide que format_date et doit être utilisée lorsque vous formatez la valeur d’un champ (colonne). Elle ne peut pas être utilisée pour des dates calculées ou des dates dans des variables de type chaîne. Exemples : format_date_field(“pubdate”, “yyyy.MM.dd”)

format_number(v, template)

class calibre.utils.formatter_functions.BuiltinFormatNumber[source]

format_number(v, template) – formate le nombre v en utilisant un modèle (template) de formatage Python comme « {0:5.2f} » ou « {0:,d} » ou « ${0:5,.2f} ». Le nom du champs (field_name) dans le modèle doit être un 0 (zéro) (le « {0: » dans les exemples ci-dessus). Regardez le langage des modèles et la documentation Python pour plus d’exemples.Vous pouvez omettre le « {0: » au début et le « } » à la fin si le modèle contient uniquement un format. Retourne une chaîne vide si le formatage échoue.

human_readable(v)

class calibre.utils.formatter_functions.BuiltinHumanReadable[source]

human_readable(v) – renvoie une chaine représentant le nombre v en Ko, Mo, Go, etc.

rating_to_stars(value, use_half_stars)

class calibre.utils.formatter_functions.BuiltinRatingToStars[source]

rating_to_stars(value, use_half_stars) – Retourne la note sous forme de chaîne de caractères étoilés. La valeur est un nombre entre 0 et 5. Mettre use_half_stars à 1 si vous voulez des caractères demi-étoiles pour les colonnes de classement personnalisées qui ne sont pas des entiers, par exemple 2.5.

urls_from_identifiers(identifiers, sort_results)

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[source]

urls_from_identifiers(identifiers, sort_results) – donne une liste d’identifiants séparée par des virgules, où un identifiant est une paire de valeurs séparées par des deux points (name:id_value), renvoie une liste d’URL HTML séparées par des virgules, générées à partir des identifiants. La liste n’est pas triée si sort_results vaut 0 (caractère ou nombre), sinon elle est triée par ordre alphabétique du nom de l’identifiant. Les URL sont générées de la même manière que la colonne d’identifiants intégrée lorsqu’elle est affichée dans Détails du livre.

Get values from metadata

annotation_count()

class calibre.utils.formatter_functions.BuiltinAnnotationCount[source]

annotation_count() – renvoie le nombre total d’annotations de tous types attachées au livre en cours. Cette fonction ne fonctionne que dans l’interface graphique.

approximate_formats()

class calibre.utils.formatter_functions.BuiltinApproximateFormats[source]

approximate_formats() – retourne une liste de formats, séparées par des virgules, qui, à un certain moment, on été associés avec le livre. Il n’y a aucune garantie que cette liste est correcte, bien qu’elle le soit probablement. Cette fonction peut-être appelée sous forme de modèle de programme en utilisant le modèle « {:”approximate_formats()”} ». Notez que ces noms de formats sont toujours en majuscule, comme dans EPUB. Cette fonction n’est utilisable que dans l’interface utilisateur. Si vous désirez utiliser cette valeur dans « Enregistrer sous… » ou « Envoyer vers le périphérique connecté », vous devez créer une colonne personnalisée de type « Colonne construite à partir d’autres colonnes » et utiliser cette fonction dans le modèle de la colonne. Vous pourrez ensuite utiliser la valeur de la colonne dans vos modèles d’enregistrement / d’envoi.

author_links(val_separator, pair_separator)

class calibre.utils.formatter_functions.BuiltinAuthorLinks[source]

author_links(val_separator, pair_separator) – retourne une chaîne contenant une liste d’auteurs et les valeurs de ce lien d’auteur dans la forme auteur1 val_separator author1link pair_separator auteur2 val_separator author2link etc. Un auteur est séparé de la valeur de son lien par la chaîne val_separator sans espaces ajoutés. Les paires author:linkvalue sont séparées par la chaîne d’argument pair_separator sans espaces ajoutés. C’est à vous de choisir des chaines de Il vous incombe de choisir des chaines de séparateur qui n’apparaissent pas dans des noms ou des liens d’auteur. Un auteur est inclus même si le lien d’auteur est vide.

author_sorts(val_separator)

class calibre.utils.formatter_functions.BuiltinAuthorSorts[source]

author_sorts(val_separator) – renvoie une chaîne contenant une liste des valeurs de tri de l’auteur pour les auteurs du livre. Le tri est celui des métadonnées auteur (différente de l’author_sort dans les livres). La liste renvoyée a la forme tri auteur 1 val_separator tri auteur 2 etc. Les valeurs de tri auteur dans cette liste sont dans le même ordre que les auteurs du livre. Si vous voulez les espaces autour du val_separator incluez les alors dans la chaîne separator

booksize()

class calibre.utils.formatter_functions.BuiltinBooksize[source]

booksize() – retourne la valeur de la taille du champ. Cette fonction n’est utilisable que dans l’interface utilisateur. Si vous désirez utiliser cette valeur dans « Enregistrer sous… » ou « Envoyer vers le périphérique connecté », vous devez créer une colonne personnalisée de type « Colonne construite à partir d’autres colonnes » et utiliser cette fonction dans le modèle de la colonne. Vous pourrez ensuite utiliser la valeur de la colonne dans vos modèles d’enregistrement / d’envoi.

connected_device_name(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[source]

connected_device_name(storage_location) – si un périphérique est connecté alors retournez le nom du périphérique, sinon retournez la chaîne vide. Chaque emplacement de stockage sur un périphérique peut avoir un nom différent. Les noms des emplacements sont « main », « carda » et « cardb ». Cette fonction ne fonctionne que dans l’interface graphique.

connected_device_uuid(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[source]

connected_device_uuid(storage_location) – si un périphérique est connecté alors retournez l’uuid du périphérique (id unique), sinon retournez la chaîne vide. Chaque emplacement de stockage sur un périphérique a un uuid différent. Les noms des emplacements sont “main”, “carda” et “cardb”. Cette fonction ne fonctionne que dans l’interface graphique.

current_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[source]

current_library_name() – retourne le dernier nom du chemin de la bibliothèque calibre actuelle. Cette fonction peut-être appelée en mode de modèle de programme en utilisant le modèle « {:”current_library_name()”} ».

current_library_path()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[source]

current_library_path() – retourne le chemin de la bibliothèque calibre actuelle. Cette fonction peut-être appelée en mode de modèle de programme en utilisant le modèle « {:”current_library_path()”} ».

current_virtual_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[source]

current_virtual_library_name() – retourne le nom de la bibliothèque virtuelle actuelle s’il y en a une, sinon la chaîne vide. La casse du nom de la bibliothèque est préservée. Exemple : « program:current_virtual_library_name() ».

field(lookup_name)

class calibre.utils.formatter_functions.BuiltinField[source]

field(lookup_name) – retourne le champ de métadonnées nommé par lookup_name

formats_modtimes(date_format)

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[source]

formats_modtimes(date_format) – retourne une liste séparée par des virgules d’éléments séparés par deux-points représentant les dates de modification pour les formats d’un livre. Le paramètre date_format indique comment la date doit être formatée. Regardez la fonction date_format pour des détails. Vous pouvez utiliser la fonction select pour obtenir le mod time pour un format spécifique.Veuillez noter que les noms de format sont toujours en majuscules, comme dans EPUB.

formats_paths()

class calibre.utils.formatter_functions.BuiltinFormatsPaths[source]

formats_paths() – retourne une liste séparées par des virgules d’éléments séparés par deux points représentant le chemin absolu vers les formats d’un livre. Vous pouvez utiliser cette fonction pour obtenir le chemin d’un format spécifique. Notez que ces noms de formats sont toujours en majuscule, comme dans EPUB.

formats_sizes()

class calibre.utils.formatter_functions.BuiltinFormatsSizes[source]

formats_sizes() – renvoie une liste séparée par des virgules correspondant à des articles représentant les tailles en octets des formats d’un livre. Vous pouvez utiliser la fonction select pour obtenir la taille d’un format spécifique. Notez que ces noms de formats sont toujours en majuscule, comme dans EPUB.

has_cover()

class calibre.utils.formatter_functions.BuiltinHasCover[source]

has_cover() – renvoie Yes si le livre a une couverture, sinon renvoie une chaine vide

is_marked()

class calibre.utils.formatter_functions.BuiltinIsMarked[source]

is_marked() – vérifie si le livre est “marqué” dans calibre. S’il l’est, il renvoie alors la valeur de la marque, soit « true », soit la liste des marques nommées séparées par des virgules. Retourne “” si le livre n’est pas marqué.

language_codes(lang_strings)

class calibre.utils.formatter_functions.BuiltinLanguageCodes[source]

language_codes(lang_strings) – retourne les codes de langue pour les chaines passées dans lang_strings. Les chaines doivent être dans la langue actuelle de localisation. Lang_strings est une liste séparée par des virgules.

language_strings(lang_codes, localize)

class calibre.utils.formatter_functions.BuiltinLanguageStrings[source]

language_strings(lang_codes, localize) – retourne les chaines des codes de langue passés dans lang_codes. Si localisation est égale à zéro, retourne les chaines en anglais. Si localisation est différente de zéro, retourne les chaines dans la langue utilisée actuellement. Lang_codes est une liste séparée par des virgules.

ondevice()

class calibre.utils.formatter_functions.BuiltinOndevice[source]

ondevice() – renvoie Yes si « Sur le périphérique » est activé, sinon renvoie une chaîne vide. Cette fonction n’est utilisable que dans l’interface utilisateur. Si vous désirez utiliser cette valeur dans « Enregistrer sous… » ou « Envoyer vers le périphérique connecté », vous devez créer une colonne personnalisée de type « Colonne construite à partir d’autres colonnes » et utiliser cette fonction dans le modèle de la colonne. Vous pourrez ensuite utiliser la valeur de la colonne dans vos modèles d’enregistrement / d’envoi.

raw_field(lookup_name [, optional_default]))

class calibre.utils.formatter_functions.BuiltinRawField[source]

raw_field(lookup_name [, optional_default])) – renvoie le champ de métadonnées nommé par lookup_name sans appliquer de formatage. Il évalue et renvoie le second argument optionnel “default” si le champ est indéfini (“None”).

raw_field(lookup_name, separator)

class calibre.utils.formatter_functions.BuiltinRawList[source]

raw_field(lookup_name, separator) – renvoie la liste de métadonnées nommée par lookup_name sans appliquer aucun formatage ou triage et avec les éléments séparés par separator.

series_sort()

class calibre.utils.formatter_functions.BuiltinSeriesSort[source]

series_sort() – renvoie la valeur de tri des séries

user_categories()

class calibre.utils.formatter_functions.BuiltinUserCategories[source]

user_categories() – retourne une liste séparée par des virgules des catégories utilisateur contenant ce livre, séparées par des virgules . Cette fonction n’est utilisable que dans l’interface utilisateur. Si vous désirez utiliser cette valeur dans « Enregistrer sous… » ou « Envoyer vers le périphérique connecté », vous devez créer une colonne personnalisée de type « Colonne construite à partir d’autres colonnes », utiliser cette fonction dans ce modèle de colonne, et utiliser la valeur de la colonne dans vos modèles d’enregistrement / d’envoi.

virtual_libraries()

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[source]

virtual_libraries() – retourne une liste de bibliothèques virtuelles contenant ce livre, séparées par des virgules. Cette fonction n’est utilisable que dans l’interface utilisateur. Si vous désirez utiliser ces valeurs dans les modèles enregistrement-sur-disque ou envoi-vers-appareil, vous devez créer une colonne personnalisée de type « Colonne construite à partir d’autres colonnes », utiliser cette fonction dans le modèle de la colonne, et utiliser la valeur de la colonne dans vos modèles d’enregistrement/envoi.

If-then-else

check_yes_no(field_name, is_undefined, is_false, is_true)

class calibre.utils.formatter_functions.BuiltinCheckYesNo[source]

check_yes_no(field_name, is_undefined, is_false, is_true) – vérifie la valeur du champ oui/non nommé par la clé de recherche field_name pour une valeur spécifiée par les paramètres, en retournant « yes » si une correspondance est trouvée, sinon en retournant une chaîne vide. Définissez le paramètre is_undefined, is_false, ou is_true à 1 (le nombre) pour vérifier cette condition, sinon mettez le à 0. Exemple : check_yes_no(« #bool », 1, 0, 1) renvoie « yes » si le champ oui/non « #bool » est soit indéfini (ni True ni False) ou True. Plus d’un des champs is_undefined, is_false ou is_true peut être mis à 1. Cette fonction est généralement utilisée par les fonctions test() ou is_empty().

contains(val, pattern, texte si correspondance, texte si pas de correspondance)

class calibre.utils.formatter_functions.BuiltinContains[source]

contains(val, pattern, texte si correspondance, texte si pas de correspondance) – vérifie si val contient des correspondances pour l’expression régulière pattern. Renvoie texte si correspondance si des correspondances sont trouvées, sinon il renvoie texte si pas de correspondance

field_exists(field_name)

class calibre.utils.formatter_functions.BuiltinFieldExists[source]

field_exists(field_name) – vérifie si un champ (colonne) nommé field_name existe, en retournant “1” si oui et “” si non.

ifempty(val, texte si vide)

class calibre.utils.formatter_functions.BuiltinIfempty[source]

ifempty(val, texte si vide) – renvoie val si val n’est pas vide, sinon renvoie texte si vide

test(val, text if not empty, text if empty)

class calibre.utils.formatter_functions.BuiltinTest[source]

test(val, text if not empty, text if empty) – renvoie texte si non vide si val n’est pas vide, sinon renvoie texte si vide

Iterating over values

first_non_empty(value [, value]*)

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[source]

first_non_empty(value [, value]*) – retourne la première valeur non vide. Si toutes les valeurs sont vides, alors la chaîne vide est retournée. Vous pouvez avoir autant de valeurs que vous voulez.

lookup(val, [pattern, field,]+ else_field)

class calibre.utils.formatter_functions.BuiltinLookup[source]

lookup(val, [pattern, field,]+ else_field) – comme switch, sauf que les arguments sont des noms de champs (métadonnées) et non du texte. La valeur du champ approprié sera récupérée et utilisée. Notez que les colonnes composites étant des champs, vous pouvez utiliser cette fonction dans un champ composite pour utiliser la valeur d’un autre champ composite. Cette fonction est extrêmement utile pour construire des chemins de sauvegarde variables.

switch(val, [pattern, value,]+ else_value)

class calibre.utils.formatter_functions.BuiltinSwitch[source]

switch(val, [pattern, value,]+ else_value) – pour chaque paire pattern, value, vérifie si val correspond à l’expression régulière pattern et si oui, retourne cette value. Si aucun motif ne correspond, alors else_value est retourné. Vous pouvez avoir autant de paires pattern, value que vous le souhaitez.

switch_if([test_expression, value_expression,]+ else_expression)

class calibre.utils.formatter_functions.BuiltinSwitchIf[source]

switch_if([test_expression, value_expression,]+ else_expression) – pour chaque paire « test_expression, value_expression », vérifie si test_expression est vrai (non vide) et si c’est le cas, renvoie le résultat de value_expression. Si aucune test_expression n’est Vraie, le résultat de else_expression est renvoyé. Vous pouvez avoir autant de paires « test_expression, value_expression » que vous le souhaitez.

List lookup

identifier_in_list(val, id_name [, found_val, not_found_val])

class calibre.utils.formatter_functions.BuiltinIdentifierInList[source]

identifier_in_list(val, id_name [, found_val, not_found_val]) – traite val comme une liste d’identifiants séparés par des virgules. Un identifiant a le format « id_name:value ». Le paramètre « id_name » est le texte de l’identifiant à rechercher, soit « id_name » soit « id_name:regexp ». Dans le premier cas, la recherche s’effectue s’il existe un identifiant correspondant à ce nom d’utilisateur. Dans le second cas, la recherche se fait si le « id_name » correspond à un identifiant et que l’expression rationnelle correspond à la valeur de l’identifiant. Si « found_val » et « not_found_val » sont fournis, alors s’il y a une correspondance, il retourne « found_val », sinon il retourne « not_found_val ». Si « found_val » et « not_found_val » ne sont pas fournis, alors s’il y a une correspondance, alors la paire « identifier:value » est retournée, sinon une chaîne vide.

in_list(val, separator, [ pattern, found_val, ]+ not_found_val)

class calibre.utils.formatter_functions.BuiltinInList[source]

in_list(val, separator, [ pattern, found_val, ]+ not_found_val) – Traite val comme une liste d’éléments séparés par un séparateur, si le motif correspond à l’une des valeurs de la liste, retourne found_val. Si le motif ne correspond à aucune valeur de la liste, retourne not_found_val. Les paires pattern et found_value peuvent être répétées autant de fois que souhaité. Les patterns sont vérifiés dans l’ordre. La valeur found_val de la première correspondance est retournée. Alias : in_list(), list_contains()

list_item(val, index, separator)

class calibre.utils.formatter_functions.BuiltinListitem[source]

list_item(val, index, separator) – interprète la valeur comme une liste d’articles séparée par separator, retourne l’article index. Le premier article est le nombre zéro. Le dernier article peut être retourné en utilisant list_item(-1,separator). Si l’article n’est pas dans liste, alors une valeur vide est renvoyée. Le séparateur a la même signafication que dans la fonction count.

select(val, key)

class calibre.utils.formatter_functions.BuiltinSelect[source]

select(val, key) – interprète la valeur comme une liste d’éléments séparés par des virgules, les éléments étant « id:value ». Trouve la paire dont l’id est égal à key, et retourne la valeur correspondante. Retourne la chaîne vide si aucune correspondance n’est trouvée.

str_in_list(val, separator, [string, found_val, ]+ not_found_val)

class calibre.utils.formatter_functions.BuiltinStrInList[source]

str_in_list(val, separator, [string, found_val, ]+ not_found_val) – Traite val comme une liste d’éléments séparés par un séparateur, si la chaîne de caractères correspond à l’une des valeurs de la liste, elle renvoie found_val. La comparaison est exacte (pas de contenu) et n’est pas sensible à la casse. Les paires string et found_value peuvent être répétées autant de fois que souhaité. Les patterns sont vérifiés dans l’ordre. La valeur found_val de la première correspondance est renvoyée.

List manipulation

count(separator)

class calibre.utils.formatter_functions.BuiltinCount[source]

count(separator) – interprète la valeur comme une liste d’articles séparés par separator, retourne le nombre d’articles dans la liste. Beaucoup de listes utilisent une virgule comme séparateur, mais les auteurs utilisent l’esperluette. Exemples: {tags:count(,)}, {authors:count(&)}. Alias : count(), list_count()

list_count_matching(list, pattern, separator)

class calibre.utils.formatter_functions.BuiltinListCountMatching[source]

list_count_matching(list, pattern, separator) – interprète “list” comme une liste d’éléments séparés par “separator”, en retournant le nombre d’éléments dans la liste qui correspondent à l’expression régulière “pattern”. Alias : list_count_matching(), count_matching()

list_difference(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListDifference[source]

list_difference(list1, list2, separator) – restitue une liste en supprimant de list1 tout élément trouvé dans list2 en utilisant une comparaison insensible à la casse. Les éléments dans list1 et list2 sont séparés par le séparateur, comme le sont les éléments dans la liste restituée.

list_equals(list1, sep1, list2, sep2, yes_val, no_val)

class calibre.utils.formatter_functions.BuiltinListEquals[source]

list_equals(list1, sep1, list2, sep2, yes_val, no_val) –retourne yes_val si la liste 1 et la liste 2 contiennent les mêmes éléments, sinon renvoie no_val. Les éléments sont déterminés en éclatant chaque liste à l’aide du séparateur approprié (sep1 ou sep2). L’ordre des éléments dans la liste n’est pas pris en compte. La comparaison est insensible à la casse.

list_intersection(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListIntersection[source]

list_intersection(list1, list2, separator) – restitue une liste en supprimant de list1 tout élément non trouvé dans list2 en utilisant une comparaison insensible à la casse. Les éléments dans list1 et list2 sont séparés par le séparateur, comme le sont les éléments dans la liste restituée.

list_join(with_separator, list1, separator1 [, list2, separator2]*)

class calibre.utils.formatter_functions.BuiltinListJoin[source]

list_join(with_separator, list1, separator1 [, list2, separator2]*) – retourne une liste faite en joignant les éléments des listes sources (list1 etc) en utilisant with_separator entre les éléments de la liste résultante. Les éléments de chaque liste[123…] source sont séparés par le séparateur[123…] associé. Une liste peut contenir zéro valeur. Il peut s’agir d’un champ comme publisher qui n’a qu’une seule valeur, c’est à dire une liste d’un seul élément. Les doublons sont supprimés en utilisant une comparaison insensible à la casse. Les éléments sont retournés dans l’ordre où ils apparaissent dans les listes sources. Si les éléments des listes ne diffèrent que par la casse des lettres, c’est la dernière qui est utilisée. Tous les séparateurs peuvent comporter plus d’un caractère.
Exemple :

program:

list_join(“#@#”, $authors, “&”, $tags, “,”)

Vous pouvez utiliser list_join sur les résultats des appels précédents à list_join comme suit :

program:

a = list_join(“#@#”, $authors, “&”, $tags, “,”); b = list_join(“#@#”, a, “#@#”, $#genre, “,”, $#people, “&”)

Vous pouvez utiliser des expressions pour générer une liste. Par exemple, supposons que vous vouliez des éléments pour auteurs et #genre, mais avec le genre changé en mot “Genre“ suivi de la première lettre du genre, c’est-à-dire que le genre “Fiction” devient “GenreF”. Ce qui suit fera l’affaire :

program:

list_join(“#@#”, $authors, “&”, list_re($#genre, “,”, “^(.).*$”, “Genre: 1”), “,”)

list_re(src_list, separator, include_re, opt_replace)

class calibre.utils.formatter_functions.BuiltinListRe[source]

list_re(src_list, separator, include_re, opt_replace) – Construit une liste en séparant d’abord src_list en éléments utilisant le caractère de séparation. Pour chaque élément dans la liste, vérifie s’il correspond à include_re. Si c’est le cas, alors il l’ajoute à la liste à renvoyer. Si opt_replace n’est pas une chaîne vide, alors effectue le remplacement avant d’ajouter l’élément à la liste renvoyée.

list_re_group(src_list, separator, include_re, search_re [, group_template]+)

class calibre.utils.formatter_functions.BuiltinListReGroup[source]

list_re_group(src_list, separator, include_re, search_re [, group_template]+) – Comme list_re sauf que les remplacements ne sont pas optionnels. Elle utilise re_group(list_item, search_re, group_template, …) pour effectuer les remplacements sur la liste résultante.

list_remove_duplicates(list, separator)

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[source]

list_remove_duplicates(list, separator) – retourne une liste faite en supprimant les éléments en double dans la liste des sources. Si les éléments ne diffèrent qu’au cas par cas, le dernier d’entre eux est renvoyé. Les éléments de la liste source sont séparés par un séparateur, tout comme les éléments de la liste retournée.

list_sort(liste, direction, separator)

class calibre.utils.formatter_functions.BuiltinListSort[source]

list_sort(liste, direction, separator) – retourne une liste triée en utilisant un tri insensible à la casse. Si la direction est zéro, la liste est triée de manière ascendante, sinon descendante. Les éléments des listes 1 et 2 sont séparés par le séparateur, comme ceux de la liste retournée.

list_split(list_val, sep, id_prefix)

class calibre.utils.formatter_functions.BuiltinListSplit[source]

list_split(list_val, sep, id_prefix) – divise la list_val en valeurs séparées en utilisant “sep”, puis affecte les valeurs à des variables nommées “id_prefix_N” où N est la position de la valeur dans la liste. Le premier élément a la position 0 (zéro). La fonction renvoie le dernier élément de la liste. Exemple : split(“one:two:foo”, “:”, “var”) est équivalent à var_0 = “one” ; var_1 = “two” ; var_2 = “foo”.

list_union(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListUnion[source]

list_union(list1, list2, separator) – restitue une liste créée de la fusion des éléments dans list1 et list2, supprimant les doublons en utilisant une comparaison insensible à la casse. Si la casse des éléments diffère, celle de list1 est utilisée. Les éléments dans list1 et list2 sont séparés par le séparateur, comme ceux de la liste restituée. Alias: list_union(), merge_lists()

range(start, stop, step, limit)

class calibre.utils.formatter_functions.BuiltinRange[source]

range(start, stop, step, limit) – renvoie une liste de nombres générés en bouclant sur la plage spécifiée par les paramètres start, stop, et step, avec une longueur maximale de limit. La première valeur produite est “start”. Les valeurs suivantes next_v sont current_v+step. La boucle continue pendant que next_v < stop en supposant que le pas est positif, sinon pendant que next_v > stop. Une liste vide est produite si start échoue le test : start>=stop si step est positif. La limite définit la longueur maximale de la liste et a une valeur par défaut de 1000. Les paramètres start, step et limit sont facultatifs. L’appel de range() avec un seul argument spécifie stop. Deux arguments spécifient start et stop. Trois arguments spécifient start, stop et step. Quatre arguments spécifient start, stop, le step et limit. Exemples : 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)->”1,3”. range(1,5,2,5)->”1,3”. range(1,5,2,1)->error(limite dépassée).

subitems(val, start_index, end_index)

class calibre.utils.formatter_functions.BuiltinSubitems[source]

subitems(val, start_index, end_index) – Cette fonction est utilisée pour séparer des listes d’éléments comme les genres. Elle interprète la valeur comme une liste d’éléments séparés par des virgules, où chaque élément est une liste séparée par des points. Retourne une nouvelle liste composée en trouvant d’abord tous les éléments séparés par des points, puis pour chacun de ces éléments en extrayant les composantes entre start_index et end_index avant de les recombiner. Le premier élément dans une liste séparée par des points a un index de 0. Si un index est négatif, alors il part de la fin de la liste. Comme cas spécial, un fin_index de zéro est considéré comme étant la longueur de la liste. Des exemples utilisant le mode de modèle basique et supposant une valeur de #genre de « A.B.C » : {#genre:subitems(0,1)} retourne « A ». {#genre:subitems(0,2)} retourne « A.B ». {#genre:subitems(1,0)} retourne « B.C ». Supposant une valeur de #genre de « A.B.C, D.E.F », {#genre:subitems(0,1)} retourne « A, D ». {#genre:subitems(0,2)} retourne « A.B, D.E »

sublist(val, start_index, end_index, séparator)

class calibre.utils.formatter_functions.BuiltinSublist[source]

sublist(val, start_index, end_index, séparator) – interprète la valeur comme une liste d’éléments séparés par séparator, retournant une nouvelle liste composée des éléments compris entre start_index et fin_index. Le premier élément est le numéro 0. Si un index est négatif, alors il part de la fin de la liste. Comme cas spécial, un fin_index de zéro est considéré comme étant la longueur de la liste. Des exemples utilisant le mode de modèle basique et supposant que la colonne des étiquettes (qui sont séparées par des virgules) contient « A, B, C » : {tags:sublist(0,1,\,)} retourne « A ». {tags:sublist(-1,0,\,)} retourne « C ». {tags:sublist(0,-1,\,)} retourne « A, B ».

Other

arguments(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinArguments[source]

arguments(id[=expression] [, id[=expression]]*) – Utilisé dans un modèle stocké pour récupérer les arguments passés dans l’appel. Il déclare et initialise à la fois des variables locales, en fait des paramètres. Les variables sont positionnelles ; elles obtiennent la valeur du paramètre donné dans l’appel dans la même position. Si le paramètre correspondant n’est pas fourni dans l’appel, alors les arguments attribuent à cette variable la valeur par défaut fournie. S’il n’y a pas de valeur par défaut, alors la variable est définie comme une chaîne vide.

assign(id, val)

class calibre.utils.formatter_functions.BuiltinAssign[source]

assign(id, val) – assigne val à id, puis retourne val. id doit être un identifiant, pas une expression. Cette fonction peut souvent être remplacée par l’opérateur =.

globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinGlobals[source]

globals(id[=expression] [, id[=expression]]*) – Récupère les « variables globales » qui peuvent être passées dans le formateur. Il déclare et initialise les variables locales avec les noms des variables globales passées. Si la variable correspondante n’est pas fournie dans les variables globales transmises, il lui attribue la valeur par défaut fournie. S’il n’y a pas de valeur par défaut, alors la variable est définie comme une chaîne vide.

print(a[, b]*)

class calibre.utils.formatter_functions.BuiltinPrint[source]

print(a[, b]*) – affiche les arguments sur la sortie standard. À moins que vous ne lanciez calibre depuis la ligne de commande (calibre-debug -g), la sortie ira dans un trou noir.

Recursion

eval(modèle)

class calibre.utils.formatter_functions.BuiltinEval[source]

eval(modèle) – évalue le modèle, en passant les variables locales (celles assignées) à la place des métadonnées du livre. Cela permet en utilisant le traitement du modèle de construire des résultats complexes à partir de variables locales. Parce que les caractères { et } sont spéciaux, vous devez utiliser [[ pour le caractère { et ]] pour le caractère } ; ils sont convertis automatiquement. Notez également que les préfixes et les suffixes (la syntaxe |prefix|suffix) ne peuvent pas être utilisés dans les arguments de cette fonction lorsque le mode modèle de programme est utilisé.

template(x)

class calibre.utils.formatter_functions.BuiltinTemplate[source]

template(x) – évalue x comme un modèle. L’évaluation est effectuée dans son propre contexte, cela veut dire que les variables ne sont pas partagées entre l’appelant et l’évaluation du modèle. Comme les caractères { et } sont spéciaux, vous devez utiliser [[ pour le caractère { et ]] pour le caractère }; ils sont convertis automatiquement. Par exemple, template(“[[title_sort]]”) évaluera le modèle {title_sort} et retournera sa valeur. Notez également que les préfixes et les suffixes (la syntaxe |prefix|suffix syntax) ne peuvent pas être utilisés dans les arguments de cette fonction lorsque le mode modèle de programme est utilisé.

Relational

cmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinCmp[source]

cmp(x, y, lt, eq, gt) – compare x et y après les avoir convertis en nombres. Retourne lt si x < y. Retourne eq si x == y. Sinon, retourne gt. Dans de nombreux cas, les opérateurs de comparaison numérique (>#, <#, ==# etc) peuvent remplacer cette fonction.

first_matching_cmp(val, cmp1, result1,]+, else_result)

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[source]

first_matching_cmp(val, cmp1, result1,]+, else_result) – compare « val < cmpN » dans la séquence, retourne resultN pour la première comparaison qui réussit. Retourne un autre résultat si aucune comparaison ne réussit. Exemple: first_matching_cmp(10,5, »petit »,10, »moyen »,15, »grand », »géant ») retourne « grand ». Le même exemple avec une première valeur de 16 retourne « géant ».

strcmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmp[source]

strcmp(x, y, lt, eq, gt) – effectue une comparaison insensible à la casse de x et y en tant que chaînes de caractères. Retourne lt si x < y. Retourne eq si x == y. Sinon, retourne gt. Dans de nombreux cas, les opérateurs de comparaison lexicale (>, <, == etc) peuvent remplacer cette fonction.

strcmpcase(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmpcase[source]

strcmpcase(x, y, lt, eq, gt) – effectue une comparaison sensible à la casse de x et y en tant que chaînes de caractères. Retourne lt si x < y. Retourne eq si x == y. Sinon, retourne gt.
Note : Ce n’est PAS le comportement par défaut utilisé par calibre, par exemple, dans les opérateurs de comparaison lexicale (==, >, <, etc.). Cette fonction pourrait provoquer des résultats inattendus, utilisez de préférence strcmp() lorsque cela est possible.

String case changes

capitalize(val)

class calibre.utils.formatter_functions.BuiltinCapitalize[source]

capitalize(val) – renvoie val en lettres capitales

lowercase(val)

class calibre.utils.formatter_functions.BuiltinLowercase[source]

lowercase(val) – renvoie val en miniscule

titlecase(val)

class calibre.utils.formatter_functions.BuiltinTitlecase[source]

titlecase(val) – renvoie val en casse titre

uppercase(val)

class calibre.utils.formatter_functions.BuiltinUppercase[source]

uppercase(val) – renvoie val en majuscule

String manipulation

character(character_name)

class calibre.utils.formatter_functions.BuiltinCharacter[source]

character(character_name) – renvoie le caractère nommé par character_name. Par exemple, character(“newline”) renvoie un caractère de nouvelle ligne (”n”). Les noms de caractères pris en charge sont “newline”, “return”, “tab” et “backslash”.

re(val, pattern, replacement)

class calibre.utils.formatter_functions.BuiltinRe[source]

re(val, pattern, replacement) – renvoie le champ après avoir appliquer l’expression régulière. Toutes les instances de pattern seront remplacées par replacement. Comme tout dans calibre, celles-ci sont des expressions régulières compatibles Python

re_group(val, pattern [, template_for_group]*)

class calibre.utils.formatter_functions.BuiltinReGroup[source]

re_group(val, pattern [, template_for_group]*) – renvoie une chaîne faite en appliquant le modèle d’expression régulière à la valeur et en remplaçant chaque instance assortie par la chaîne calculée en remplaçant chaque groupe assorti par la valeur retournée par le modèle correspondant. La valeur assortie originale pour le groupe est disponible comme $. En mode programmation de modèle, comme pour le modèle et les fonctions eval, vous utiliserez [[pour { et ]] pour}. L’exemple suivant en mode programmation de modèle recherche des séries avec plus d’un mot et des majuscules dans le premier mot : {series:”re_group($, « (S* )(.*) », « [[$:uppercase()]] », « [[$]] »)”}

shorten(val, left chars, middle text, right chars)

class calibre.utils.formatter_functions.BuiltinShorten[source]

shorten(val, left chars, middle text, right chars) – Retourne une version raccourcie de val, constituée par les left chars caractères du début de val, suivis par middle text et enfin par right chars de la fin de la chaîne. left chars et right chars doivent être des entiers. Par exemple, supposons que le titre du livre est Anciennes Lois Anglaises aux Temps d’Ivanhoe, et que vous voulez que ce titre rentre dans un espace maximal de 15 caractères. Si vous utilisez {title:shorten(9,-,5)}, le resultat sera Ancienn L-anhoe. Si la longueur du champ est inférieure à caractères à gauche + caractères à droite + la longueur de texte du milieu, alors le champ ne sera pas modifié. Par exemple, le titre Le Dôme ne serait pas modifié.

strcat(a [, b]*)

class calibre.utils.formatter_functions.BuiltinStrcat[source]

strcat(a [, b]*) – peut avoir tout nombre d’arguments. Renvoie la chaîne constituée par la concaténation de tous les arguments

strcat_max(max, string1 [, prefix2, string2]*)

class calibre.utils.formatter_functions.BuiltinStrcatMax[source]

strcat_max(max, string1 [, prefix2, string2]*) – Retourne la chaîne de caractères formée de la concaténation des arguments. La valeur retournée est initialisée à string1. Les paires Prefix,string sont ajoutées au bout de la valeur tant que la taille de la chaîne de sortie est plus petite que max. String1 est retournée même si string1 est plus grande que max. Vous pouvez passer autant de paires de prefix,string que vous le souhaitez.

strlen(a)

class calibre.utils.formatter_functions.BuiltinStrlen[source]

strlen(a) – Retourne la longueur de la chaîne de caractères passée en argument

substr(ch, start, end)

class calibre.utils.formatter_functions.BuiltinSubstr[source]

substr(ch, start, end) – renvoie les caractères de début jusqu’à fin de la chaîne ch. Le premier caractère dans la chaine ch est le caractère 0. Si fin est négatif, cela indique que le comptage des caractères est effectué à partir de la droite. Si end est à zéro, cela indique le dernier caractère. Par exemple, substr(“12345”, 1, 0) renvoie “2345”, et substr(“12345”, 1, -1) renvoie “234”.

swap_around_articles(separator)

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[source]

swap_around_articles(separator) – renvoie la val avec les articles déplacés à la fin. La valeur peut être une liste, auquel cas chaque membre de la liste est traité. Si la valeur est une liste, alors vous devez fournir le séparateur de valeur de liste. Si aucun séparateur n’est fourni, la valeur est traitée comme une valeur unique et non comme une liste.

swap_around_comma(val)

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[source]

swap_around_comma(val) – pour une valeur au format « B, A », renvoie « A B ». Ceci est très utile pour la conversion des noms du format LN, FN vers FN LN. Si il n’y a pas de virgule, la fonction renvoie val non modifiée

to_hex(val)

class calibre.utils.formatter_functions.BuiltinToHex[source]

to_hex(val) – renvoie la chaîne encodée en hexadécimal. Ceci est utile lors de la construction d’URLs calibre.

transliterate(a)

class calibre.utils.formatter_functions.BuiltinTransliterate[source]

transliterate(a) - Restitue une chaîne en alphabet latin représentant approximativement le son des mots de la chaîne source. Par exemple, avec la source « Фёдор Миха́йлович Достоевский » la fonction renverra « Fiodor Mikhailovich Dostoievskii ».

Template database functions

book_count(query, use_vl)

class calibre.utils.formatter_functions.BuiltinBookCount[source]

book_count(query, use_vl) – retourne le nombre de livres trouvés en cherchant la requête. Si use_vl est 0 (zéro) alors les bibliothèques virtuelles sont ignorées. Cette fonction ne peut être utilisée que dans l’interface graphique.

book_values(column, query, sep, use_vl)

class calibre.utils.formatter_functions.BuiltinBookValues[source]

book_values(column, query, sep, use_vl) – renvoie une liste des valeurs contenues dans la colonne « column », séparées par « sep », dans les livres trouvés en recherchant « query ». Si use_vl vaut 0 (zéro), les bibliothèques virtuelles sont ignorées. Cette fonction ne peut être utilisée que dans l’interface graphique.

extra_file_modtime(file_name, format_string)

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[source]

extra_file_modtime(file_name, format_string) – retourne l’heure de modification du fichier supplémentaire « file_name » dans le dossier « data/ » du livre s’il existe, sinon -1. La date de modification est formatée selon « format_spec » (voir format_date() pour plus de détails). Si « format_spec » est une chaîne vide, elle retourne l’heure de modification sous la forme d’un nombre de secondes en virgule flottante depuis Epoch. L’Epoch dépend du système d’exploitation. Cette fonction ne peut être utilisée que dans l’interface graphique.

extra_file_names(sep [, pattern])

class calibre.utils.formatter_functions.BuiltinExtraFileNames[source]

extra_file_names(sep [, pattern]) – retourne une liste séparée de « sep » de fichiers supplémentaires dans le dossier « data/ » du livre. Si le paramètre optionnel « pattern », une expression régulière, est fourni, alors la liste est filtrée sur les fichiers qui correspondent à « pattern ». La correspondance est insensible à la casse. Cette fonction ne peut être utilisée que dans l’interface graphique.

extra_file_size(file_name)

class calibre.utils.formatter_functions.BuiltinExtraFileSize[source]

extra_file_size(file_name) – retourne la taille en octets du fichier supplémentaire « file_name » dans le dossier « data/ » du livre s’il existe, sinon -1. Cette fonction ne peut être utilisée que dans l’interface graphique.

get_link(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinGetLink[source]

get_link(field_name, field_value) – récupère le lien pour le champ « field_name » avec la valeur « field_value ». S’il n’y a pas de lien attaché, il renvoie “”. Exemple : get_link(“tags”, “Fiction”) renvoie le lien attaché a l’étiquette « Fiction ».

get_note(field_name, field_value, plain_text)

class calibre.utils.formatter_functions.BuiltinGetNote[source]

get_note(field_name, field_value, plain_text) – récupère la note pour le champ « field_name » avec la valeur « field_value ». Si « plain_text » est vide, le HTML de la note est retourné. Si « plain_text » n’est pas vide, la note est retournée en texte brut. Si la note n’existe pas, le système renvoie “” dans les deux cas. Exemple : get_note(“tags”, “Fiction”, “”) renvoie le code HTML de la note attachée a l’étiquette « Fiction ».

has_extra_files([pattern])

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[source]

has_extra_files([pattern]) – retourne le nombre de fichiers supplémentaires, sinon “” (la chaîne vide). Si le paramètre optionnel « pattern » (une expression régulière) est fourni, alors la liste est filtrée sur les fichiers qui correspondent à « pattern » avant que les fichiers ne soient comptés. La correspondance est insensible à la casse. Cette fonction ne peut être utilisée que dans l’interface graphique.

has_note(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinHasNote[source]

has_note(field_name, field_value) – renvoie “1” si la valeur « field_value » dans le champ « field_name » a une note attachée, “” sinon. Exemple : has_note(“tags”, “Fiction”) renvoie “1” si le tag « Fiction » a une note attachée, sinon “”.

other

set_globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinSetGlobals[source]

set_globals(id[=expression] [, id[=expression]]*) – Définit les « variables globales » qui peuvent être passées au formateur. Les variables globales reçoivent le nom de l’id passé. La valeur de l’id est utilisée sauf si une expression est fournie.

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=('Inconnu(e)',), other=None, template_cache=None, formatter=None)[source]

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)[source]

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>>)[source]

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()[source]

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)[source]

Set all identifiers. Note that if you previously set ISBN, calling this method will delete it.

set_identifier(typ, val)[source]

If val is empty, deletes identifier of type typ

standard_field_keys()[source]

return a list of all possible keys, even if this book doesn’t have them

custom_field_keys()[source]

return a list of the custom fields in this book

all_field_keys()[source]

All field keys known by this instance, even if their value is None

metadata_for_field(key)[source]

return metadata describing a standard or custom field.

all_non_none_fields()[source]

Return a dictionary containing all non-None metadata fields, including the custom ones.

get_standard_metadata(field, make_copy)[source]

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)[source]

return a dict containing all the standard field metadata associated with the book.

get_all_user_metadata(make_copy)[source]

return a dict containing all the custom field metadata associated with the book.

get_user_metadata(field, make_copy)[source]

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)[source]

store custom field metadata into the object. Field is the key name not the label

set_user_metadata(field, metadata)[source]

store custom field metadata for one column into the object. Field is the key name not the label

remove_stale_user_metadata(other_mi)[source]

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)[source]

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)[source]

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)[source]

Returns the tuple (display_name, formatted_value)

to_html()[source]

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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »

- Tout à propos de l’utilisation des expressions régulières dans calibre

Tout à propos de l’utilisation des expressions régulières dans calibre

Les expressions régulières sont des fonctions utilisées à différents endroits dans calibre pour effectuer des manipulations élaborées sur le contenu et sur les métadonnées de vos livres numériques. Ce tutoriel est une introduction légère pour vous permettre de démarrer dans l’utilisation des expressions régulières de calibre.

Contenu

- Tout d’abord, un mot d’avertissement et un mot d’encouragement

- Où pouvez-vous utiliser les expressions régulières dans calibre ?

- Que diable est une expression régulière ?

- Prendre soin d’expliquer ?

- Cela ne semble pas trop mauvais. Ensuite ?

- He, joli ! Cela commence à avoir du sens !

- Bien, ces caractères spéciaux sont bien jolis et tout, mais qu’en est-il si je veux faire correspondre à un point ou à un point d’interrogation ?

- Bien, quels sont les ensembles les plus utiles ?

- Mais si j’avais quelques chaînes variables que je voudrais faire correspondre, les choses deviennent-elles compliquées ?

- Vous avez manqué…

- Au début vous disiez qu’il y avait un moyen de rendre les expressions régulières insensibles à la casse ?

- Je pense que je commence à comprendre ces expressions régulières maintenant… comment dois-je les utiliser dans calibre?

- Conversions

- Ajout de livres

- Edition des métadonnées par lot

- Mémento

- Crédits

Tout d’abord, un mot d’avertissement et un mot d’encouragement

Ceci est, inévitablement, en train de devenir quelque peu technique- après tout, les expressions régulières sont des outils techniques pour faire une chose technique. Je vais devoir utiliser un certain jargon et des concepts qui peuvent sembler compliqués ou alambiqués. Je vais essayer d’expliquer ces concepts aussi clairement que possible, mais je ne peux vraiment pas le faire sans les utiliser du tout… Donc, ne soyez pas découragé par un tel jargon, comme j’ai essayé d’expliquer tout ce qui est nouveau. Et alors que les expressions régulières en elles mêmes peuvent paraître comme une arcane, de la magie noire (ou, pour être plus prosaïque, une chaîne aléatoire d’un charabia de lettres et de signes). Je promets qu’elles n’ont pas toutes cette complexité. Même ceux qui comprennent bien les expressions régulières ont des difficultés à lire les plus complexes, mais les écrire n’est pas une difficulté- vous construisez l’expression pas à pas. Aussi, faites un pas et suivez-moi dans le trou du lapin.

Où pouvez-vous utiliser les expressions régulières dans calibre ?

Il y a dans calibre quelques endroits utilisant des expressions régulières. Il y en Rechercher & remplacer dans les options de conversion, dans la détection de métadonnées à partir des noms de fichiers dans les paramètres d’importation ainsi que dans Rechercher et remplacer lors de l’édition des métadonnées de livres par lot. L’éditeur de livre de calibre peut également utiliser des expressions régulières dans sa fonctionnalité Rechercher et remplacer. Enfin, vous pouvez utiliser des expressions régulières lors de la recherche dans la liste des livres de calibre et lors de la recherche dans la visionneuse de calibre.

Que diable est une expression régulière ?

Une expression régulière est une manière de décrire des ensembles de chaînes. Une expression régulière simple peut correspondre à un nombre différent de chaînes. C’est ce qui rend les expressions régulières si puissantes – elles sont une manière concise de décrire un nombre potentiellement grand de variations.

Note

J’utilise ici le mot chaîne dans le sens où il est utilisé dans les langages de programmation : une chaîne d’un ou plusieurs caractères, caractères incluant des caractères habituels, des nombres, de la ponctuation et ce qu’on appelle les whitespace (retours de ligne, tabulations, etc…). Veuillez noter que généralement, les caractères majuscules et minuscules ne sont pas considérés les mêmes, donc « a » est un caractère différent de « A » et ainsi de suite. Dans calibre, les expressions régulières sont sensibles à la casse dans la Barre de recherche, mais pas dans les options de conversion. Il y a une manière pour rendre chaque expression régulière insensible à la casse, mais nous en discuterons plus tard. Ça devient compliqué parce que les expressions régulières tiennent compte des variations des chaînes qu’elles font correspondre, ainsi une expression peut correspondre à des chaînes multiples, ce qui est pourquoi les gens ne prennent pas la peine de les employer du tout. Plus sur cela dans un moment.

Prendre soin d’expliquer ?

Bien, c’est pourquoi nous sommes ici. D’abord, c’est le concept le plus important dans les expressions régulières : Une chaîne en-soi est une expression régulière qui correspond à elle même. Ceci est pour dire, si je voulais faire correspondre la chaîne "Hello, World!" en utilisant une expression régulière, l’expression régulière à utiliser devra être "Hello, World!". Et oui, c’est vraiment aussi simple. Vous noterez, cependant, que cela correspond uniquement à la chaîne exacte "Hello, World!", pas par ex. à "Hello, wOrld!" ou à "hello, world!" ou à n’importe quelle autre variation.

Cela ne semble pas trop mauvais. Ensuite ?

La suite est le commencement des choses vraiment intéressantes. Vous souvenez-vous quand j’ai dit que les expressions régulières peuvent correspondre à plusieurs chaînes ? C’est là où ça les rendait un peu plus compliquées. Disons, comme une sorte d’exercice plus pratique, que le livre numérique que vous vouliez convertir avait de méchantes notes de bas de page comptant les pages, comme « Page 5 de 423 ». Évidemment le nombre de pages devrait augmenter de 1 à 423, donc vous devriez avoir à faire correspondre 423 chaînes, juste ? Faux, en fait: les expressions régulières vous permettent de définir un ensemble de caractères qui correspond : Pour définir un ensemble, vous mettez tous les caractères que vous voulez y voir figurer entre crochets. Aussi, par exemple, l’ensemble [abc]``devra correspondre soit au caractère  "a", soit au "b" ou au "c". *Les ensembles correspondront toujours uniquement à un des caractères dans l'ensemble*, c'est à dire, si vous voulez faire correspondre tous les caractères minuscules, vous devrez utiliser l'ensemble ``[a-z] pour les caractères minuscules et majuscules vous devrez utiliser l’ensemble [a-zA-Z] et ainsi de suite. Vous voyez l’idée ? Ainsi, évidemment, en utilisant l’expression Page [0-9] de 423 vous serez capable de correspondre aux 9 premières pages, donc par conséquent les expressions en ont besoin de trois : la seconde expression Page [0-9][0-9] de 423 correspondra aux numéros de pages à deux chiffres, et je suis sûr que vous pouvez deviner à quoi ressemblera la troisième expression. Oui, allez-y. Mettez-la par écrit.

He, joli ! Cela commence à avoir du sens !

J’espère que vous vous dites cela. Mais accrochez vous, maintenant cela va devenir encore mieux ! Nous avons juste dit qu’en utilisant les ensembles, nous pouvons correspondre à un de plusieurs caractères en une fois. Mais vous pouvez même répéter un caractère ou un ensemble, réduisant le nombres d’expressions nécessaires pour gérer l’exemple numéro de page ci dessus à une. Oui, UNE ! Enthousiaste ? Vous devriez l’être ! Cela fonctionne comme ceci : certains caractères qu’on appelle spéciaux, « + », « ? » et « * », répète l’élément simple le précédant. (Élément signifie soit un caractère seul, un ensemble de caractères, une séquence d’échappement ou un groupe) (nous aborderons ces deux derniers plus tard)- en bref, n’importe quelle entité simple dans une expression régulière). Ces caractères sont appelés des caractères de remplacement ou des quantificateurs. Pour être plus précis, « ? » correspond à 0 ou 1 de l’élément précédant, « * » correspond à 0 ou plus de l’élément précédant et « + » correspond à 1 ou plus de l’élément précédant. Quelques exemples : l’expression a? correspondrait soit à «  » (qui est la chaîne vide, pas strictement utile dans ce cas) ou « a », l’expression « a* » correspondrait à « « , « a », « aa » ou n’importe quel nombre de a dans une ligne, finalement, l’expression « a+ » correspondrait à « a », « aa » ou n’importe quel nombre de a dans une ligne(Notez : il ne correspondra pas à une chaîne vide !). Même affaire avec les ensembles, l’expression [0-9]+ correspondra à chaque numéro entier qu’il y a ! Je sais à quoi vous êtes occupé à penser, et vous avez raison : si vous utilisez cela dans le cas ci-dessus de la correspondance du numéro de page, est-ce que cette simple expression ne pourrait pas correspondre à tous les numéros de page ? Oui, l’expression [0-9]+ de 423 correspondra à chaque numéro de page dans ce livre !

Note

Une note sur ces quantificateurs : Ils essaient généralement de correspondre au plus de texte possible, aussi soyez prudent lors de leur utilisation. Ceci est appelé « un comportement glouton »- Je suis sûr que vous comprenez pourquoi. Ils deviennent problématiques quand vous, disons, essayez de correspondre à une balise. Considérons, par exemple, la chaîne "<p class="calibre2">Titre ici</p>" et disons que vous voulez correspondre à la balise d’ouverture (la partie entre la première paire de chevrons, un peu plus sur les balises plus tard). Vous devez penser que l’expression <p.*> devrait correspondre à cette balise, mais actuellement, elle correspond à l’entièreté de la chaîne ! (Le caractère « . » est un autre caractère spécial. Il correspond à n’importe quoi excepté les sauts de ligne, aussi, essentiellement, l’expression .* devrait correspondre à n’importe quelle ligne à laquelle vous pensez). A la place, essayez d’utiliser <p.*?> qui rend le quantificateur "*" non-glouton. Cette expression ne correspondrait qu’à la première balise d’ouverture, comme attendu. Il y a en fait une autre manière d’y parvenir : L’expression <p[^>]*> correspondra à la même balise d’ouverture - vous verrez pourquoi après la prochaine section. Notez juste qu’il y a assez fréquemment plus d’une manière d’écrire une expression régulière.

Bien, ces caractères spéciaux sont bien jolis et tout, mais qu’en est-il si je veux faire correspondre à un point ou à un point d’interrogation ?

Vous pouvez bien sûr faire cela : insérez juste une barre oblique inversée devant tout caractère spécial et il sera interprété comme un caractère littéral, sans aucune autre signification. Cette paire de barres obliques inversées suivie d’un caractère seul est appelée une séquence d’échappement, et l’action de mettre une barre oblique inversée devant un caractère spécial est appelée un échappant de ce caractère. Une séquence d’échappement est interprétée comme un élément seul. Il y a bien sûr des séquences d’échappement qui font plus que juste échapper des caractères spéciaux, par exemple "\t" signifie une tabulation. Nous verrons certaines des séquences d’échappement plus tard. Ah, et temps que nous y sommes, au sujet de ces caractères spéciaux: Considérez n’importe quel caractère dont nous discutons dans cette introduction comme ayant une certaine fonction pour être spéciaux et de ce fait devant être échappé si vous voulez le caractère littéral.

Bien, quels sont les ensembles les plus utiles ?

Je savais que vous demanderiez. Certains des ensembles utiles sont [0-9] correspondant à un nombre unique, [a-z] correspondant à un caractère minuscule unique, [A-Z] correspondant à un caractère majuscule unique, [a-zA-Z] correspondant à un caractère unique et [a-zA-Z0-9] correspondant à une lettre ou un nombre unique. Vous pouvez aussi utiliser une séquence d’échappement comme raccourci :

\d

est équivalent à [0-9]

\w

est équivalent à [a-zA-Z0-9_]

\s

est équivalent à tout espace blanc

Note

« L’espace blanc » est un terme désignant tout ce qui ne sera pas imprimé. Ces caractères comprennent l’espace, la tabulation, le saut de ligne, le saut de page, le retour chariot, les espaces insécables, etc.

Note

Les jeux de majuscules et de minuscules peuvent correspondre à la fois aux majuscules et aux minuscules si le paramètre permettant de rendre les recherches insensibles à la casse est activé. De tels paramètres se trouvent, par exemple, dans Préférences->Recherche dans calibre lui-même et sur le panneau de recherche dans la visionneuse de calibre Livre numérique ainsi que dans l’outil calibre Editer un livre.

Comme dernière note sur les ensembles, vous pouvez aussi définir un ensemble de n’importe quel caractère*mais* ceux dans l’ensemble. Vous faites cela en incluant le caractère "^" comme le tout premier caractère dans l’ensemble. Donc, [^a] correspondra à n’importe quel caractère en excluant le « a ». C’est ce qu’on appelle complémenter l’ensemble. Ces sténographies de séquences d’échappement que nous avons vues plus tôt peuvent aussi être complémentées : "\D" signifie n’importe quel caractère qui n’est pas un nombre, donc étant équivalent à [^0-9]. Les autres sténographies peuvent être complémentées par, vous le devinez, l’utilisation de la majuscule respective à la place de la minuscule. Aussi, revenons à l’exemple <p[^>]*> de la section précédente, maintenant vous pouvez voir que le jeu de caractères utilise des essais pour correspondre à n’importe quel caractère excepté un chevron fermant.

Mais si j’avais quelques chaînes variables que je voudrais faire correspondre, les choses deviennent-elles compliquées ?

N’ayez pas peur, la vie est belle et facile. Considérons cet exemple : Le livre que vous convertissez a « Titre » écrit sur chaque page impaire et « Auteur » écrit sur chaque page paire. Harmonieux à l’impression, pas vrai ? Mais dans les livres numériques, c’est ennuyeux. Vous pouvez grouper ensemble les expressions dans des parenthèses normales et le caractère "|" vous permettra de correspondre soit à l’expression à sa droite ou à l’expression à sa gauche. Combinez les et vous avez fini. Trop rapide pour vous ? Ok, tout d’abord, nous groupons les expressions pour les pages paires et impaires, donc nous obtenons (Titre)(Auteur)` comme nos deux expressions nécessaires. Maintenant nous rendons les choses plus simples en utilisant la barre verticale ("|" est appelé le caractère barre verticale) : si vous utilisez l’expression (Titre|Auteur) vous obtiendrez une correspondance pour « Titre » (sur les pages impaires) ou vous correspondrez à « Auteur » (sur les pages paires). Bien, n’était-ce pas facile ?

Vous pouvez, bien sûr, utiliser la barre verticale sans utiliser les parenthèses de regroupement, aussi bien. Vous vous souvenez quand je disais que les caractères spéciaux répétaient l’élément le précédant, et bien pour le caractère barre verticale, c’est différent.L’expression « Titre|Auteur » permet de sélectionner soit auteur, soit titre, comme sur l’exemple précédent avec les parenthèses. Le caractère barre verticale sélectionne l’expression devant lui ou celle derrière lui. Donc si on veut sélectionner les chaînes « calibre » et « Calibre », il faudra utiliser l’expression (c|C)alibre, où le groupement assure qu’uniquement le « c » sera sélectionné. Si on avait utilisé c|Calibre, alors on aurait obtenu c ou Calibre, ce qui n’est pas ce qui était attendu. Donc en cas de doute, il est nécessaire de grouper avec les parenthèses.

Vous avez manqué…

… Attendez une minute, il y a une dernière chose, vraiment bien, que vous pouvez faire avec les groupes. A partir d’un groupe que vous avez déjà récupéré, vous pouvez utiliser une référence à ce groupe plus tard dans l’expression. Les groupes trouvés sont numérotés à partir de 1, et vous pouvez y faire référence en échappant le numéro du groupe auquel vous vous référencé, donc le cinquième groupe devra être référencé comme \5. Aussi si vous recherchiez après ([^ ]+) \1 dans la chaîne « Test Test », vous correspondriez à la chaîne entière !

Au début vous disiez qu’il y avait un moyen de rendre les expressions régulières insensibles à la casse ?

Oui, je l’ai fait, merci de votre attention et de me l’avoir rappelé. Vous pouvez dire à calibre comment vous voulez que certaines choses soient traitées en utilisant quelque chose appelée bannière. Vous incluez des bannières dans votre expression en utilisant la construction spéciale (?bannières vont ici) où, évidemment, vous devrez remplacer « bannières vont ici » avec les bannières spécifiques que vous voulez. Pour ignorer la casse, la bannière est le i, donc vous incluez test(?i) dans votre expression. Dès lors, (?i)test correspondra à Test, tEst, TEst et toutes les variations imaginables.

Une autre bannière utile laisse le point corresponde à n’importe quel caractère du tout, incluant le saut de ligne, la bannière s. Si vous voulez utiliser de multiples bannières dans une expression, mettez les juste dans la même déclaration : ('?is) ignorera la casse fera que le point correspondra à tout. Quelle bannière vous placez en premier n’est pas important, (?si) sera équivalent à celui ci-dessus.

Je pense que je commence à comprendre ces expressions régulières maintenant… comment dois-je les utiliser dans calibre?

Conversions

Commençons avec les paramètres de conversion, qui est vraiment bien. Dans la partie Rechercher & remplacer, on peut utiliser une regexp (abréviation pour expression régulière) qui décrit la chaîne qui sera remplacée pendant la conversion. La partie magique, c’est l’assistant. En cliquant sur l’assistant, on obtient la prévisualisation de ce que calibre « verra » pendant le processus de conversion. Défilez vers le bas jusqu’à la chaque vous voulez supprimer, sélectionnez-la et copiez-la, collez la dans le champ regexp au dessus de la fenêtre. S’ils ont des parties variables, comme les numéros de page ou autre, utilisez les ensembles et les quantificateurs pour couvrir ceux-ci, et pendant que vous y êtes, souvenez-vous d’échapper les caractères spéciaux, s’il y en a certains. Appuyez le bouton libellé Test et calibre mettra en évidence les parties qu’il devrait remplacer s’il utilisait la regexp. Une fois que vous êtes satisfait, appuyez OK et convertissez. Soyez prudent si votre source de conversion à des balises comme dans cet exemple:

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

(honteusement tiré de ce fil de discussion [https://www.mobileread.com/forums/showthread.php?t=75594"]). Vous devriez également enlever certaines des balises . Dans cet exemple, je recommanderais de commencer par la balise <b class="calibre2">, maintenant vous devez terminé avec la balise fermante correspondante (les balises ouvrantes sont <balise>, les balises fermantes sont </tag>), qui est simplement le prochain </b> dans ce cas. (Référez-vous à un bon manuel HTML ou posez la question sur le forum si vous n’êtes pas au clair sur ce point). La balise ouvrante peut être décrite en utilisant <b.*?>, la balise fermante en utilisant </b>, donc nous pourrions retirer entièrement ce qui se trouve entre ces deux balises en utilisant <b.*?>.*?</b>. Mais utiliser cette expression serait une mauvaise idée, parce qu’elle retire tout ce qui se trouve encadré par des balises <b>- (qui, par ailleurs, rend le texte encadré en gras), et c’est un pari gagné que nous enlèverons des parties du livre de cette façon. À la place, inclure également le commencement de la chaîne encadrée, faisant l’expression régulière <b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b> Le \s avec les quantificateurs sont inclus ici à la place d’utiliser explicitement les espaces comme considéré dans la chaîne pour saisir toutes les variations de la chaîne qui pourraient survenir. Souvenez-vous de vérifier qu’est ce que calibre retirera pour vous assurer que vous ne retirez pas des portions que vous voulez garder si vous testez une nouvelle expression. Si vous vérifiez uniquement une occurrence, vous pourriez manquer une disparité ailleurs dans le texte. Notez également que si vous enlevez accidentellement plus ou moins de balises que vous ne le vouliez réellement, calibre essaye de réparer le code endommagé après avoir fait le retrait.

Ajout de livres

Une autre chose pour laquelle vous pouvez utiliser les expressions régulières est l’extraction des métadonnées à partir des noms de fichier. Vous pouvez trouver cette fonctionnalité dans la partie Ajout de livres des préférences. Il y a une fonctionnalité spéciale ici : vous vous utiliser les noms de champ pour les champs de métadonnées, par exemple (?P<title>) indiquera que calibre utilise cette parie de la chaîne comme titre du livre. Les noms de champs permis sont listés dans la fenêtre, accompagné d’un autre joli champ de test. Un exemple : disons que vous voulez importer un tas de fichiers nommés Textes Classiques: La Divine Comédie par Dante Alighieri.mobi. (Evidemment, ceci est déjà dans votre bibliothèque, puisque nous aimons tous la poésie italienne classique) ou Les épiques de la Science Fiction : La Trilogie Fondation par Isaac Asimov.epub. Ceci est évidemment un schéma de nommage dont calibre ne peut extraire aucunes données significatives - son expression standard pour l’extraction de métadonnées est (?P<title>.+) - (?P<author>[^_]+). Une expression régulière qui fonctionnera ici devra être [a-zA-Z]+: (?P<title>.+) par (?P<author>.+). Merci de noter que, à l’intérieur du groupe pour le champ métadonnées , vous devez utiliser des expressions pour décrire ce à quoi le champ correspond réellement. Et notez également que, lors de l’utilisation de la zone de test de calibre, vous devez ajouter l’extension de fichier, sinon vous n’aurez pas de résultats du tout, malgré l’utilisation d’une expression correcte.

Edition des métadonnées par lot

La dernière partie est le Rechercher et remplacer par expression régulière dans les champs de métadonnées. Vous pouvez accéder à ceci en sélectionnant de multiples livres dans la bibliothèque et en utilisant l’édition de métadonnées par lot. Soyez très prudent en utilisant cette dernière fonction, vu qu’elle peut faire de Très Vilaines Choses à votre bibliothèque. Vérifiez par deux fois que vos expressions font ce que vous attendez d’elles en utilisant les champs de test, et marquez uniquement les livres que vous voulez réellement changer ! Dans le mode recherche par expression régulière, vous pouvez rechercher dans un champ, remplacer le texte avec quelque chose et même écrire le résultat dans un autre champ. Un exemple pratique : disons que votre bibliothèque contient les livres de la série Dune de Frank Herbert, nommés selon le modèle Dune 1 - Dune, Dune 2 - Dune Messiah et ainsi de suite. Maintenant vous voulez obtenir Dune dans le champ série Vous pouvez faire cela en recherchant après (.*?) \d+ - .* dans le champ titre et en le remplaçant avec \1 dans le champ séries. Vous voyez ce que je veux dire ? C’est une référence au premier groupe avec lequel vous allez remplacer le champ série. Maintenant que vous avez toutes vos séries prêtes, vous avez uniquement besoin de faire une autre recherche de .*? - et le remplacer avec "" (une chaîne vide) encore dans le champ titre, et vos métadonnées sont toutes ordonnées et rangées. De cette manière, au lieu de remplacer le champ entier, vous pouvez également apposer ou ajouter au début au champ, ainsi, si vous voulez le titre de livre à ajouter au début avec des infos de séries, vous pourriez faire cela également. Comme vous l’avez suremment noté, il y a une case à cocher marquée Sensible à la casse, ainsi vous ne devrez pas employer des bannières pour sélectionner le comportement ici.

Voilà, c’est juste pour conclure cette très courte introduction aux expressions régulières. Si tout va bien je vous aurai montré assez que pour vous permettre au moins de démarrer et de vous donner l’envie de continuer d’apprendre par vous-même- un bon point de départ serait la Documentation Python pour les regexps [https://docs.python.org/library/re.html].

Un dernier mot de l’avertissement, cependant : Les regexps sont puissantes, mais également vraiment facile à devenir erronées. calibre fournit des possibilités de vraiment grandes d’essai pour voir si vos expressions se comportent que vous vous y attendez à . Employez-les. Essayez de ne pas vous tirer dans le pied. (Dieu, j’aime cette expression…). Mais si vous vous, en dépit de l’avertissement, blessez votre pied (ou toutes autres parties du corps), essayez d’apprendre de celui-ci.

Mémento

- Mémento pour la syntaxe regexp

- Les classes de caractères
- Les classes de caractères abrégées
- Les quantificateurs
- La gourmandise
- L’alternative
- L’exclusion
- Les ancres
- Groupes
- Les assertiions
- La récursion
- Les caractères spéciaux
- Les métacaractères
- Modes

Crédits

Remerciements pour l’aide avec les astuces, les correction et analogues :

- ldolse

- kovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

Pour plus d’informations sur les regexps, voir The Python User Manual [https://docs.python.org/library/re.html]. La bibliothèque d’expressions régulières actuellement utilisée par calibre est : regex [https://bitbucket.org/mrabarnett/mrab-regex/src/hg/] qui supporte plusieurs améliorations utiles par rapport à la bibliothèque standard Python.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »

- Tout à propos de l’utilisation des expressions régulières dans calibre »

- Mémento pour la syntaxe regexp

Mémento pour la syntaxe regexp

Cet aide-mémoire résume les parties du moteur regexp les plus couramment utilisées/difficilement mémorisables disponibles dans la plupart des parties de calibre.

Contenu

- Les classes de caractères

- Les classes de caractères abrégées

- Les quantificateurs

- La gourmandise

- L’alternative

- L’exclusion

- Les ancres

- Groupes

- Les assertiions

- La récursion

- Les caractères spéciaux

- Les métacaractères

- Modes

Les classes de caractères

Les classes de caractères sont utiles pour représenter différents groupes de caractères, succinctement.

Exemples :

Représentation

Classe

[a-z]

Lettres minuscules. Ne comprend pas les caractères accentués et les ligatures

[a-z0-9]

Lettres minuscules de a à z ou chiffres de 0 à 9

[A-Za-z-]

Lettre majuscule ou minuscule, ou un tiret. Pour inclure le tiret dans une classe, il faut le mettre au début ou à la fin pour ne pas le confondre avec le tiret qui spécifie une plage de caractères

[^0-9]

Tout caractère sauf un chiffre. Le circonflexe (^) placé en début de classe exclut les caractères de la classe (classe complémentée)

[[a-z]--[aeiouy]]

Les consonnes minuscules. Une classe peut être incluse dans une classe. Les caractères -- excluent ce qui les suit

[\w--[\d_]]

Toutes les lettres (y compris les caractères accentués étrangers). Les classes abrégées peuvent être utilisées à l’intérieur d’une classe

Exemple:

<[^<>]+> to select an HTML tag

Les classes de caractères abrégées

Représentation

Classe

\d

Un chiffre (identique à [0-9])

\D

Tout caractère non numérique (identique à [^0-9])

\w

Un caractère alphanumérique plus le souligné ([a-zA-Z0-9_]) y compris les caractères accentués et les ligatures

\W

Tout caractère « non-word »

\s

Espace, espace insécable, tabulation, retour ligne

\S

Tout caractère « non-whitespace »

.

N’importe quel caractère sauf le retour ligne. Il faut cocher la case « dot all » ou utiliser la regexp (?s) pour inclure le caractère de retour de ligne

Les quantificateurs

Quantificateur

Nombre occurrences de l’expression précédant le quantificateur

?

0 ou 1 occurrence de l’expression. Identique à {0,1}

+

1 ou plusieurs occurrences de l’expression. Identique à {1,}

*

0, 1 ou plusieurs occurrences de l’expression. Identique à {0,}

{n}

Exactement n occurrences de l’expression

{min,max}

Nombre d’occurrences compris entre les valeurs minimales et maximales incluses

{min,}

Nombre d’occurrences compris entre la valeur minimale incluse et l’infini

{,max}

Nombre d’occurrences compris entre 0 et la valeur maximale incluse

La gourmandise

Par défaut, avec les quantificateurs, le moteur d’expressions rationnelles est gourmand (greedy) : il étend la sélection autant que possible. Cela réserve des surprises, au début. ? suit un quantificateur pour le rendre non gourmand (lazy). Éviter d’en mettre deux dans la même expression, le résultat peut être imprévisible.

Attention à l’imbrication des quantificateurs, comme, par exemple, le motif (a*)* : il augmente de façon exponentielle les traitements.

L’alternative

Le caractère | dans une expression régulière est un OR logique. Cela signifie que soit l’expression précédente soit l’expression suivante peut correspondre.

L’exclusion

Méthode 1
motif_à_exclure(*SKIP)(*FAIL)|motif_à_sélectionner

Exemple :

"Blabla"(*SKIP)(*FAIL)|Blabla
sélectionne Blabla, dans les chaînes Blabla ou « Blabla ou Blabla », mais pas dans « Blabla ».

Méthode 2
motif_à_exclure\K|(motif_à_sélectionner)

"Blabla"\K|(Blabla)

sélectionne Blabla, dans les chaînes Blabla ou « Blabla ou Blabla », mais pas dans « Blabla ».

Les ancres

Une ancre est un moyen de faire correspondre une position logique dans une chaîne, plutôt qu’un caractère. Les ancres les plus utiles pour le traitement de texte sont :

\b

Désigne une limite de mot, càd une transition depuis un espace à caractère non-espace. Par exemple, vous pouvez utiliser \bsurd pour correspondre à the surd mais pas absurd.

^

Correspond au début d’une ligne (en mode multi lignes, qui est le mode par défaut)

$

Correspond à la fin d’une ligne (en mode multi lignes, qui est le mode par défaut)

\K

Réinitialise l’endroit de début de la sélection à sa position dans le motif. Certains moteurs de regexp (mais pas celui de calibre) n’autorisent pas les assertions arrière de longueur variable, et notamment avec des quantificateurs. Lorsque l’on peut utiliser K avec ces moteurs-là, il permet aussi de s’affranchir de cette limite en écrivant l’équivalent d’une assertions arrière positive de longueur variable.

Groupes

(expression)

Groupe de capture, qui mémorise la sélection et qui peut être rappelée par la suite dans les motifs « rechercher » ou « remplacer » par n, où n est le numéro d’ordre du groupe de capture (en commençant à 1 dans l’ordre de lecture)

(?:expression)

Groupe qui ne mémorise pas la sélection

(?>expression)

Groupe atomique : dès que l’expression est satisfaite, le moteur de regexp passe à la suite, et si le reste du motif échoue, il ne fera pas marche arrière pour essayer d’autres combinaisons avec l’expression. Les groupes atomiques ne capturent pas.

(?|expression)

Groupe de réinitialisation de branche : les branches des alternatives inclues dans l’expression partagent les mêmes numéros de groupe

(?<name>expression)

Groupe nommé « name ». La sélection peut être rappelée par la suite dans le motif rechercher par (?P=name) et dans le remplacer par \g<name>. Deux groupes différents peuvent utiliser le même nom.

Les assertiions

Assertion

Signification

?=

Assertion avant positive (à placer après la sélection)

?!

Assertion avant négative (à placer après la sélection)

?<=

Assertion arrière positive (à placer avant la sélection)

?<!

Assertion arrière négative (à placer avant la sélection)

Les assertions avant et arrière ne consomment pas de caractère, elles sont de longueur nulle et ne capturent pas. Ce sont des groupes atomiques : dès que l’assertion est satisfaite, le moteur de regexp passe à la suite, et si le reste du motif échoue, il ne fera pas marche arrière à l’intérieur de l’assertion pour essayer d’autres combinaisons.

Lorsque l’on cherche plusieurs correspondances dans une chaîne, à la position de départ de chaque tentative de correspondance, une assertion arrière peut inspecter les caractères situés avant la position actuelle. Par conséquent, sur la chaîne 123, le motif (?<=\d)\d (un chiffre précédé d’un chiffre) devrait, en théorie, sélectionner 2 et 3. En revanche, \d\K\d ne peut sélectionner que 2, car la position de départ après la première sélection est immédiatement avant 3, et il n’y a plus assez de chiffres pour une seconde correspondance. De même, \d(\d) ne capture que 2. Dans la pratique du moteur de regexp de calibre, l’assertion arrière positive se comporte de la même façon, et ne sélectionne que 2, contrairement à ce que prévoit la théorie.

On peut placer des groupes à l’intérieur des assertions, mais la capture est rarement utile. Néanmoins, si elle est utile, il faudra être très prudent dans l’utilisation d’un quantificateur dans une assertion arrière : la gourmandise associée à l’absence de marche arrière peut donner une capture surprenante. Pour cette raison, utilisez \K plutôt qu’une assertion arrière positive quand vous avez un quantificateur (ou pire, plusieurs) dans un groupe capturant de l’assertion arrière positive.

Exemple d’assertion avant négative :

(?![^<>{}]*[>}])

Placée à la fin du motif, cette assertion empêche de sélectionner à l’intérieur d’une balise ou d’un style embarqué dans le fichier.

Lorsque c’est possible, il est toujours préférable d’ »ancrer » les assertions, pour diminuer le nombre d’étapes nécessaires à l’obtention du résultat.

La récursion

Représentation

Signification

(?R)

Récursion du motif entier

(?1)

Récursion du seul motif du groupe de capture numéroté, ici le groupe 1

La récursion consiste à s’appeler soi-même. C’est utile pour des recherches balancées, par exemple des chaînes entre apostrophes, qui peuvent contenir des chaînes entre apostrophes enchâssées. Ainsi, si au cours du traitement d’une chaîne entre apostrophes, on rencontre le début d’une nouvelle chaîne entre apostrophes, et bien on sait faire, et on s’appelle soi-même. On a alors un motif comme :

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

Pour sélectionner une chaîne entre apostrophes sans s’arrêter aux chaînes enchâssées:

“((?>[^“”]+|(?R))*[^“”]+)”

C’est également ce modèle qu’il faut suivre pour modifier les paires de balises qui peuvent s’enchâsser, telles que les balises <div>.

Les caractères spéciaux

Représentation

Caractère

\t

tabulation

\n

saut de ligne

\x20

espace (sécable)

\xa0

espace insécable

Les métacaractères

Les métacaractères sont ceux qui ont une signification spéciale pour le moteur regexp. Parmi ceux-ci, douze doivent être précédés d’un caractère d’échappement, l’antislash (\), pour perdre leur signification spéciale et redevenir un simple caractère:

^ . [ ] $ ( ) * + ? | \

Sept autres métacaractères n’ont pas besoin d’être précédés de l’antislash (mais peuvent l’être sans autre conséquence):

{ } ! < > = :

Les caractères spéciaux perdent leur statut s’ils sont utilisés à l’intérieur d’une classe (entre les crochets []). Le crochet fermant et le tiret ont un statut spécial dans une classe. En dehors de la classe, le tiret est un simple littéral, le crochet reste un métacaractère.

Le slash (/) et le croisillon (#) [carré au Québec] ne sont pas des métacaractères, a fortiori ils n’ont pas besoin d’être échappés.

Dans certains outils, comme regex101.com, avec le moteur Python, les doubles quotes ont le statut spécial de séparateur, et doivent être échappés. Ce n’est pas le cas dans l’éditeur de calibre.

Modes

(?s)

Amène le point (.) à correspondre également à des caractères de nouvelle ligne

(?m)

Rend les ancres ^ et $ à correspondre au début et à la fin de la ligne plutôt que le début et la fin de la chaîne entière.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »

- Écrire vos propres extensions pour étendre les fonctionnalités de calibre

Écrire vos propres extensions pour étendre les fonctionnalités de calibre
calibre a un design hautement modulaire. Presque toutes les fonctionnalités dans calibre viennent sous la forme d’extensions. Les extensions sont utilisées pour la conversion, pour télécharger des actualités (bien que celles-ci s’appellent des recettes), pour divers composants de l’interface utilisateur, pour connecter différents périphériques, pour traiter les fichiers lorsque vous les ajouter à calibre. Vous pouvez obtenir une liste complète de toutes les extensions intégrées dans calibre en allant dans Préférences → Avancé → Extensions.

Ici, nous vous apprendrons comment créer vos propres extensions pour ajouter de nouvelles fonctionnalités à calibre.

Contenu

- Anatomie d’une extension calibre

- Une extension d’Interface Utilisateur

- __init__.py

- ui.py

- main.py

- Obtention des ressources à partir du fichier ZIP de l’extension

- Activation de la configuration utilisateur pour votre extension

- Extensions d’édition de livre

- main.py

- Ajout des traductions à votre extension

- L’API extension

- Dépannage des extensions

- Plus d’exemples d’extensions

- Partager vos extensions avec les autres

Note

Ceci s’applique uniquement aux versions de calibre >=0.8.60

Anatomie d’une extension calibre

Une extension calibre est très simple, c’est juste un fichier ZIP qui contient un peu de code Python et toutes les autres ressources comme les fichiers images nécessaires à l’extension. Sans plus tarder, voyons un exemple basique.

Supposons que vous avez une installation de calibre que vous utilisez pour l’auto publication de divers e-documents aux formats EPUB et MOBI. Vous aimeriez que tous les fichiers générés par calibre aient leur éditeur paramétré à Salut le monde, voici comment faire. Créez un fichier nommé __init__.py (c’est un nom spécial et doit toujours être utilisé pour le fichier principal de votre extension) entrez-y le code Python suivant :

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

C’est tout. Pour ajouter ce code à calibre en tant qu’extension, exécuter simplement ce qui suit dans le dossier dans lequel vous avez créé __init__.py:

calibre-customize -b .

Note

Sous macOS, les outils en de ligne de commande sont à l’intérieur de la suite logicielle calibre, par exemple, si vous installez calibre dans /Applications les outils de ligne de commande sont dans /Applications/calibre.app/Contents/MacOS/.

Vous pouvez télécharger le plugin Hello World depuis helloworld_plugin.zip [https://calibre-ebook.com/downloads/helloworld_plugin.zip].

A chaque fois que vous utilisez calibre pour convertir un livre, la méthode de l’extension run() sera appelée et les livres convertis auront leur éditeur établi à Salut le monde. C’est une extension banale, passons à un exemple plus complexe qui habituellement ajoute un composant à l’interface utilisateur.

Une extension d’Interface Utilisateur

Cette extension sera répartie sur quelques fichiers (pour maintenir le code propre). Il vous montrera comment obtenir des ressources (des fichiers d’images ou de données) à partir du fichier extension ZIP, permettra à des utilisateurs de configurer votre extension, comment créer des éléments dans l’Interface Utilisateur de calibre et comment accéder et questionner la base de données de livres de calibre.

Vous pouvez télécharger ce plugin à partir de interface_demo_plugin.zip [https://calibre-ebook.com/downloads/interface_demo_plugin.zip].

La première chose à noter est que le fichier ZIP contient beaucoup plus de fichiers, expliqués ci-dessous, prêtez particulièrement attention à plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Un fichier texte vide utilisé pour activer la magie de l’extension multi fichiers. Ce fichier doit être présent dans toutes les extensions qui utilisent plus d’un fichier .py. Il devra être vide et son nom de fichier doit être de la forme : plugin-import-name-un_nom.txt. La présence de ce fichier vous permet d’importer du code à partir de fichiers .py présents à l’intérieur du fichier ZIP, utilisant une déclaration comme:

from calibre_plugins.some_name.some_module import some_object

Le préfixe calibre_plugins doit toujours être présent. some_name provient du nom de fichier du fichier texte vide. some_module se réfère au fichier some_module.py dans le fichier ZIP. Notez que cette importation est tout aussi puissante que les importations Python régulières. Vous pouvez créer des paquets et des sous paquets de modules .py à l’intérieur du fichier ZIP, tout comme vous le feriez normalement (en définissant __init__.py dans chaque sous dossier), et tout devrait « fonctionner correctement ».

Le nom que vous utilisez pour un_nom introduit un espace de nom global partagé par toutes les extensions. Aussi rendez le aussi unique que possible. Mais rappelez-vous qu’il doit être un identifiant Python valide(uniquement des lettres, des nombres et le tiret bas).

__init__.py

Comme précédemment, le fichier qui définit la classe de l’extension

main.py

Ce fichier contient le code actuel qui fait quelque chose d’utile

ui.py

Ce fichier définit la partie interface de l’extension

images/icon.png

L’icône pour ce plugin

about.txt

Un fichier texte contenant des informations sur le plugin

traductions

Un répertoire contenant des fichiers .mo avec les traductions de l’interface utilisateur de votre extension dans différentes langues. Voir ci-dessous pour les détails.

Maintenant regardons le code

__init__.py

Tout d’abord, l’obligatoirement __init__.py pour définir les métadonnées de l’extension :

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 seule caractéristique remarquable est le champ actual_plugin. Depuis que calibre a la ligne de commande et les interfaces de GUI, les extensions avec GUI comme celle-ci ne devrait charger aucune bibliothèques de GUI dans __init__.py. Le champ actual_plugin fait ceci pour vous, en disant à calibre que l’extension actuelle doit être trouvée dans un autre fichier à l’intérieur de votre archive ZIP, qui seront seulement chargées dans un contexte GUI.

Rappelez-vous que pour que cela fonctionne, vous devez avoir un fichier plugin-import-name-un_nom.txt dans le fichier ZIP de votre extension, comme discuté plus haut.

Il y a également quelques méthodes pour permettre la configuration utilisateur de l’extension. Celles-ci sont discutées ci-dessous.

ui.py

Maintenant regardons à ui.py qui définit le GUI habituel de l’extension. Le code source est fortement commenté et devrait être explicite :

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

La logique actuelle pour mettre en application le dialogue de démo de l’interface de l’extension.

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'])

Obtention des ressources à partir du fichier ZIP de l’extension

Le système de chargement d’une extension calibre définit quelques fonctions intégrées qui permettent d’obtenir confortablement des fichiers à partir du fichier ZIP de l’extension.

get_resources(name_or_list_of_names)

Cette fonction devra être appelée avec une liste de chemins vers les fichiers à l’intérieur du fichier ZIP. Par exemple pour accéder au fichier icon.png dans le dossier images dans le fichier ZIP, vous devriez utiliser : images/icon.png. Toujours utiliser une barre oblique comme séparateur de chemin, même sous Windows. Quand vous analysez un nom seul, la fonction retournera les octets bruts ou Aucun si le nom n’est pas trouvé dans le fichier ZIP. Si vous analysez plus d’un nom alors il renvoie un dictionnaire mappant les noms aux octets. Si un nom n’est pas trouvé, il ne sera pas présent dans le dictionnaire retourné.

get_icons(name_or_list_of_names, plugin_name=””)

Un enveloppeur pour get_resources() qui crée des objets QIcon à partir des octets bruts retournés par get_resources. Si un nom n’est pas trouvé dans le fichier ZIP, le QIcon correspondant sera nul. Afin de supporter les thèmes d’icônes, entrez le nom convivial de votre plugin comme plugin_name. Si l’utilisateur utilise un thème d’icônes avec des icônes pour votre plugin, ils seront chargés de préférence.

Activation de la configuration utilisateur pour votre extension

Pour permettre aux utilisateurs de configurer votre extension, vous devez définir trois méthodes dans votre classe de base de l’extension, is_customizable, config_widget et save_settings comme montré ci-dessous :

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 a beaucoup de manières différentes pour stocker les données de configuration (un héritage de sa longue histoire). La manière recommandée est d’utiliser la classe JSONConfig, qui stocke votre information de configuration dans un fichier .json.

Le code pour gérer le données de configuration dans l’extension démo est dans 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()

L’objet prefs est maintenant disponible dans tout le code de l’extension par un simple:

from calibre_plugins.interface_demo.config import prefs

Vous pouvez voir l’objet prefs en cours d’utilisation dans main.py :

self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

Extensions d’édition de livre

Maintenant changeons de direction pour un moment et regardons à la création d’une extension pour ajouter des outils à l’éditeur de livre calibre. L’extension est disponible ici : editor_demo_plugin.zip [https://calibre-ebook.com/downloads/editor_demo_plugin.zip].

La première étape comme pour toutes les extensions est de créer le nom du fichier txt vide d’importation, comme décrit plus haut. Nous appellerons vle fichier plugin-import-name-editor_plugin_demo.txt.

Maintenant nous créons le fichier mandataire __init__.py qui contient les métadonnées à propos de l’extension – ses nom, auteur, version, 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)

Une simple extension éditeur peut procurer de multiples outils chaque outil correspond à un seul bouton dans la barre d’outils et une entrée dans le menu Extensions dans l’éditeur. Celle-ci peut avoir des sous menus dans le cas où l’outil à de multiples actions liées.

Les outils doivent tous être définis dans le fichier main.py dans votre extension. Chaque outil est une classe qui hérite de la classe calibre.gui2.tweak_book.plugin.Tool. Regardons attentivement le main.py de l’extention démo, le code source est fortement commenté et devrait être explicite. Lisez les documents API de la classe calibre.gui2.tweak_book.plugin.Tool pour plus de détails.

main.py

Nous verrons ici la définition d’un outil simple qui multipliera toutes les tailles de police dans le livre par un nombre fourni par l’utilisateur? Cet outil démontre les différents concepts importants dont vous aurez besoin dans le développement de vos propres extensions, aussi vous devriez lire attentivement le code source (fortement commenté).

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

Décomposons main.py. Nous voyons qu’il définit une seul outil, nommé Magnify fonts. Cet outil demandera à l’utilisateur un nombre et multipliera toutes les tailles de police dans le livre par ce nombre.

La première chose importante est le nom d’outil que vous devez établir à une certaine chaîne relativement unique car elle sera employée comme clé pour cet outil.

L’entrée importante suivante est la calibre.gui2.tweak_book.plugin.Tool.create_action(). Cette méthode crée les objects QAction qui apparaissent dans la barre d’outils et le menu extensions. Elle assigne aussi, optionnellement, un raccourci clavier que l’utilisateur peut personnaliser. Le signal de déclenchement de QAction est relié à la méthode ask_user () qui demande à l’utilisateur le multiplicateur de taille de la police, et puis exécute le code de grossissement.

Le code de grossissement est bien commenté assez simple. Les choses les plus importantes à noter sont que vous obtenez une référence à la fenêtre de l’éditeur comme self.gui et l’éditeur Boss comme self.boss. Le Boss est l’objet qui contrôle l’interface utilisateur de l’éditeur. Il a beaucoup de méthodes utiles, elles sont documentées dans la classe calibre.gui2.tweak_book.boss.Boss.

Finalement, il y a self.current_container qui est ue référence au livre en cours d’édition comme un objet calibre.ebooks.oeb.polish.container.Container. Ceci représente le livre comme une collection de ses fichiers constituants HTML/CSS/image et a des méthodes commodes pour faire beaucoup de choses utiles. L’objet container et diverses fonctions de service utiles qui peuvent être réutilisées en votre code d’extension sont documentées dans Documentation API pour l’outil d’édition de livre numérique.

Ajout des traductions à votre extension

Vous pouvez avoir toutes les chaînes de l’interface utilisateur dans votre extension traduites et affichées dans n’importe quelle langue paramétrée pour l’interface utilisateur principale de calibre.

La première étape est de faire le tour du code source de votre extension et marquer toutes les chaînes visibles par l’utilisateur comme traduisible, en les entourant dans _(). Par exemple:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Utilisez alors un programme pour générer les fichiers .po à partir du code source de votre extension. Il devrait y avoir un fichier .po pour chaque langue dans laquelle vous voulez une traduction. Par exemple de.po pour l’allemand, fr.po pour le français et ainsi de suite. Vous pouvez utiliser le programme Poedit [https://poedit.net/] pour cela.

Envoyez ces fichiers .po à vos traducteurs. Une fois que vous les obtenez en retour, compilez les dans des fichiers .mo. Vous pouvez encore utilisez Poedit pour cela, ou juste faire:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Déposez les fichiers .mo dans le répertoire translations dans votre extension.

La dernière étape est de simplement appeler la fonction load_translations() en haut de vos fichiers .py d’extension. Pour des raisons de performance, vous devriez seulement appeler cette fonction dans les fichiers .py qui ont réellement des chaînes traduisibles. Aussi dans une Interface Utilisateur typique vous devriez l’appeler en haut de ui.py mais pas de __init__.py.

Vous pouvez tester les traductions de vos extensions en changeant la langue de l’interface utilisateur dans calibre dans Préférences → Interface → Apparence ou en exécutant calibre avec la variable d’environnement CALIBRE_OVERRIDE_LANG. Par exemple :

CALIBRE_OVERRIDE_LANG=de

Remplac``de`` avec le code de langue de la langue que vous voulez tester.

Pour les traductions avec des pluriels, utilisez la fonction ngettext() au lieu de _(). Par exemple :

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

L’API extension

Comme vous avez pu le constatez plus haut, une extension dans calibre est une classe. Il a différentes classes pour les différents types d’extension dans calibre. Les détails sur chaque classe, incluant la classe de base de toutes les extensions peuvent être trouvés dans Documentation API pour les extensions.

Votre extension va presque certainement employer le code de calibre. Pour apprendre comment trouver les diverses parties de fonctionnalités dans le code de base de calibre, lisez la section sur le Disposition du code calibre.

Dépannage des extensions

Tout d’abord, l’étape la plus importante est d’exécuter calibre en mode dépannage. Vous pouvez faire cela depuis la ligne de commande avec:

calibre-debug -g

Ou, dans calibre, en faisant un clic droit sur le bouton Préférences ou en utilisant le raccourci clavier Ctrl+Shift+R.

Lors de l’exécution en ligne de commande, la sortie de dépannage sera affichée dans la console, lors de l’exécution depuis calibre la sortie ira dans un fichier txt.

Vous pouvez insérer des déclarations n’importe où dans votre code d’extension, elles seront reprises dans le mode dépannage. Rappelez-vous, ceci est du Python, vous ne devriez n’avoir vraiment besoin de rien de plus que les déclarations pour corriger ;) J’ai développé entièrement calibre en utilisant juste cette technique d’élimination des imperfections.

Vous pouvez rapidement tester les changements de votre extension en utilisant la ligne de commande suivante:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Ceci arrêtera un calibre en cours d’exécution, attendez que l’arrêt soit complet, mettez alors à jour votre extension à jour dans calibre et relancez calibre.

Plus d’exemples d’extensions

Vous pouvez trouver une liste de nombreuses extensions calibre sophistiquées `ici<https://www.mobileread.com/forums/showthread.php?t=118764>`_.

Partager vos extensions avec les autres

Si vous aimeriez partager les extensions que vous avez créées avec d’autres utilisateurs de calibre, postez votre extension dans un nouveau fil de discussion sur le forum des extensions calibre [https://www.mobileread.com/forums/forumdisplay.php?f=237].

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »

- Compositions mathématiques dans des livres numériques

Compositions mathématiques dans des livres numériques

La Visionneuse de livre numérique calibre a la capacité d’afficher les mathématiques intégrées dans les livres numériques (fichiers EPUB et HTML). Vous pouvez composer directement les mathématiques avec TeX, MathML ou AsciiMath. La visionneuse de calibre utilise l’excellente librairie MathJax [https://www.mathjax.org] pour faire cela. Ceci est un bref tutoriel sur la création de livres numériques contenant des mathématiques qui fonctionnent bien avec la Visionneuse de livre numérique calibre.

Un simple fichier HTML avec des mathématiques

Vous pouvez écrire des mathématiques en ligne à l’intérieur d’un fichier HTML simple et la Visionneuse de livre numérique de calibre rendra des mathématiques correctement composées. Dans l’exemple ci-dessous, nous employons la notation de Tex pour les mathématiques. Vous verrez que vous pouvez employer des commandes normales de Tex, avec la petite mise en garde que les esperluettes et les signes moins grand que et plus grand que doivent être écrits respectivement comme &amp; &lt; et &gt; .

La première étape est de dire à calibre que ceci contiendra des mathématiques. Vous faites cela en ajoutant l’extrait de code suivant à la section <head> du HTML:

<script type="text/x-mathjax-config"></script>

C’est tout, maintenant vous pouvez entrer des mathématiques comme vous le feriez dans un fichier .tex. Par exemple, voici les équations 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>

Cet extrait ressemble à la capture d’écran suivante dans la Visionneuse de livre numérique calibre.

Les Équations de Lorenz

Le fichier HTML complet, avec plus d’équations et de mathématiques intégrées est reproduit ci-dessous. Vous pouvez convertir ce fichier HTML en EPUB dans calibre pour obtenir un livre numérique que vous pouvez distribuer facilement à d’autres personnes.

<!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>

Plus d’information

Depuis que la Visionneuse de livre numérique calibre utilise la librairie MathJax pour afficher les mathématiques, le meilleur endroit pour en savoir plus à propos des mathématiques dans les livres numériques et obtenir de l’aide est le site web de MathJax [https://www.mathjax.org].

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »
- Création de catalogues AZW3 • EPUB • MOBI

Création de catalogues AZW3 • EPUB • MOBI

L’option Créer un catalogue de calibre vous permet de créer un catalogue de votre librairie dans une variété de formats. Ce fichier d’aide décrit les options de catalogage lors de la génération d’un catalogue aux formats AZW3, EPUB et MOBI.

- Sélection des livres à cataloguer

- Sections incluses

- Préfixes

- Livres exclus

- Exclure les genres

- Autres options

- Couvertures de catalogue personnalisées

- Ressources d’aide supplémentaires

Sélection des livres à cataloguer

Si vous voulez que « toute » votre bibliothèque soit cataloguée, supprimez tout critère de recherche ou de filtrage dans votre fenêtre principale. Avec un seul livre sélectionné, tous les livres de votre bibliothèque seront des candidats à l’inclusion dans le catalogue généré. Des livres individuels peuvent être exclus par différents critères ; voir la section Exclure les genres ci-dessous pour plus d’informations.

Si vous voulez cataloguer une certaine partie de votre librairie, vous avez deux options :

- Créer un sélection multiple des livres que vous voulez cataloguer. Avec plus d’un livre sélectionné dans la fenêtre principale de calibre, seuls les livres sélectionnés seront catalogués.

- Utiliser le champ Recherche ou le Navigateur d’étiquettes pour filtrer les livres affichés. Seuls les livres affichés seront catalogués.

Pour commencer la génération d’un catalogue, cliquer sur la flèche descendante à droite de Convertir des livres > Créer un catalogue des livres de la bibliothèque calibre. Vous pouvez également ajouter un bouton Créer un catalogue à la barre d’outil dans Préférences > Interface > Barre d’outils & menus pour accéder facilement à la boîte de dialogue Générer un catalogue.

Dans Options du catalogue, sélectionnez AZW3, EPUB ou MOBI comme format de catalogue. Dans le champ Titre du catalogue, donner un nom qui sera utilisé pour le catalogue généré. Si un catalogue avec le même nom et le même format existe déjà, il sera remplacé par le catalogue nouvellement généré.

L’activation de Envoyer le catalogue automatiquement vers le périphérique connecté téléchargera le catalogue généré vers un périphérique connecté.

Sections incluses

Les sections activées par une coche seront incluses dans le catalogue généré :

- Auteurs - tout les livres, triés par auteur, présentés en format liste. Les livres ne faisant pas partie d’une série sont listés avant ceux qui sont dans une série.

- Titres - tous les livres, triés par titre, présentés en format liste.

- Séries - Tous les livres qui font partie d’une série , triés par séries, présentés en format liste.

- Genres - genres individuels présentés dans une liste, triée par Auteur et Séries.

- Récemment Ajoutés - tous les livres, triés dans l’ordre chronologique inverse. La liste inclut les livres ajoutés dans les 30 derniers jours, ensuite un listing mois par mois des livres ajoutés.

- Descriptions - page de description détaillée pour chaque livre, incluant une vignette de couverture et les commentaires. Triés par auteur, avec les livres hors séries listés avant les livres faisant partie de séries.

Préfixes

Les règles de préfixe vous permettent d’ajouter un préfixe à une liste de livres quand certains critères sont rencontrés. Par exemple, vous pouvez vouloir marquer des livres que vous avez lu avec une coche, ou des livres de votre liste de souhaits avec un X.

La case à cocher dans la première colonne active la règle. :guilabel:”Nom” est un nom de règle que vous fournissez. Champ est soit Etiquettes ou une colonne personnalisée de votre bibliothèque. Valeur est le contenu de Champ à faire correspondre. Quand une règle de préfixe est satisfaite, le livre sera marqué avec le Préfixe sélectionné.

Trois règles de préfixe ont été spécifiées dans l’exemple ci-dessus :

1. Livre lu spécifie qu’un livre avec n’importe quelle date dans une colonne personnalisée nommée Dernier lu sera préfixé avec le symbole coché.

2. L’élément Liste de souhaits spécifie que tout livre avec une étiquette Liste de souhaits sera préfixée avec le symbole X.

3. Livres de la bibliothèque spécifie que tout livre avec une valeur True (ou Oui) dans une colonne personnalisée Disponible dans la bibliothèque sera préfixé avec un symbole flèche double.

La première règle correspondante active le préfixe. des règles désactivées ou incomplètes sont ignorées.

Livres exclus

Les règles d’exclusion vous permettent de spécifier des livres qui ne devront pas être catalogués.

La case à cocher dans la première colonne active la règle. :guilabel:”Nom” est un nom de règle que vous fournissez. Champ est soit Etiquettes ou une colonne personnalisée de votre bibliothèque. Valeur est le contenu de Champ à faire correspondre. Quand une règle d’exclusion est satisfaite, le livre sera exclu du catalogue généré.

Deux règles d’exclusions ont été spécifiées dans l’exemple ci-dessous :

1. La règle Catalogues spécifie que tout livre avec une valeur Catalogue sera exclu du catalogue généré.

2. La règle des Livres Archived spécifie que tout livre avec une étiquette Archived dans la colonne personnalisée Status sera exclu du catalogue généré.

Toutes les règles sont évaluées pour chaque livre. Les règles désactivées ou incomplètes sont ignorées.

Exclure les genres

Quand le catalogue est généré, les étiquettes dans votre base de données sont utilisées comme genres. Par exemple, si vous pouvez utiliser les étiquettes Fiction et Non fiction. Ces étiquettes deviennent des genres dans le catalogue généré, avec les livres listés sous leurs listes de genre respectives basées sur les étiquettes qui leurs ont été assignées.Un livre sera listé dans chaque section genre pour laquelle il a une étiquette correspondante.

Vous pouvez avoir utilisé certaines étiquettes pour d’autres finalités, peut-être un + pour indiquer un livre lu, ou une étiquette entre crochets comme [Amazon Freebie] pour indiquer une source de livre. La regex Genres exclus vous permet de spécifier des étiquettes que vous ne voulez pas utiliser comme genre dans le catalogue généré. Le modèle par défaut regex d’exclusion \[.+\]\+ exclut toutes étiquettes de la forme [étiquette], aussi bien que d’exclure +, l’étiquette par défaut pour les livres lus, d’être utilisés comme genres dans le catalogue généré.

Vous pouvez aussi utiliser un nom d’étiquette exact dans une regex. Par exemple, [Amazon Freebie] ou [Project Gutenberg]. Si vous voulez lister de multiples étiquettes exactes pour exclusion, mettez un caractère pipe (barre verticale) entre eux : [Amazon Freebie]|[Project Gutenberg].

Résultats de la regex vous montre quelles étiquettes seront exclues quand le catalogue sera construit, basé sur les étiquettes dans votre base de données et sur le modèle regex entré. Les résultats sont modifiés lorsque vous modifiez le modèle regex.

Autres options

Couverture de Catalogue spécifie soit de générer une nouvelle couverture ou d’en utiliser une existante. Il est possible de créer une couverture personnalisée pour vos catalogues - voir Couvertures de catalogue personnalisées pour plus d’information. Si vous avez créer une couverture personnalisée que vous voulez réutiliser, sélectionner Utiliser une couverture existante. Autrement, sélectionnez Générer une nouvelle couverture.

Note de Description supplémentaire spécifie que le contenu d’une colonne personnalisée est inséré dans la page de Description, juste après la vignette de couverture. Par exemple, vous pouvez vouloir afficher la date où vous avez lu un livre pour la dernière fois en utilisant la colonne personnalisée Dernier lu. Pour une utilisation avancée de la fonction note de Description, voir ce billet dans le forum calibre [https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395].

Largeur miniature spécifie une préférence de largeur pour les vignettes de couverure inclues avec les pages de Descriptions. Les vignettes sont mises en cache pour améliorer les performances. Pour expérimenter avec différentes largeurs, essayer en générant un catalogue avec juste quelques livres jusqu’à ce que vous ayez déterminé votre largeur préférée, générer alors votre catalogue complet. La première fois qu’un catalogue est généré avec une nouvelle largeur de vignette, les performances seront moindres, mais les constructions ultérieures du catalogue prendront avantage de la mise en cache des vignettes.

Fusionner avec les commentaires spécifie une colonne personnalisée dont le contenu sera fusionnée de manière non destructive avec la métadonnée commentaires pendant la génération du catalogue.. Par exemple,, vous pouvez avoir une colonne personnalisée Bio Auteur que vous aimeriez ajouter à la métadonnée commentaires. Vous pouvez choisir d’insérer le contenu de la colonne personnalisée avant ou après la section commentaires, et optionnellement séparer le contenu ajouté avec une ligne de séparation horizontale. Les types de colonnes personnalisées éligibles incluent texte, commentaires, et composites.

Couvertures de catalogue personnalisées

Avec l” extension Generate Cover [https://www.mobileread.com/forums/showthread.php?t=124219] installée, vous pouvez créer des couvertures personnalisées pour votre catalogue. Pour installer l’extension, allez à Préférences > Avancé> Extensions > Obtenir de nouvelles extensions.

Ressources d’aide supplémentaires

Pour plus d’informations sur la fonction Catalogue de calibre, voir le forum MobileRead Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556], où vous pouvez trouver de l’information sur comment personnaliser les modèles de catalogue, et comment soumettre un rapport de bogue.

Pour poser des questions ou discuter de la fonctionnalité Catalogue de calibre avec d’autres utilisateurs, visitez le forum MobileRead Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236].

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Tutoriels »
- Bibliothèques virtuelles

Bibliothèques virtuelles

Dans calibre, une Bibliothèque virtuelle est une manière de dire à calibre d’ouvrir uniquement un sous-ensemble d’une bibliothèque normale. Par exemple, vous pouvez vouloir travailler uniquement avec les livres d’un certain auteur, ou les livres ayant seulement une certaine étiquette. Utiliser les Bibliothèques virtuelles est la manière préférable de partitionner votre grande collection de livres en de plus petites sous collections. C’est plus intéressant que de diviser votre bibliothèque en de multiples bibliothèques plus petites vu que, quand vous voulez rechercher dans votre collection entière, vous devez simplement revenir à la bibliothèque complète. Il n’y a pas de manière de rechercher simultanément au travers de multiples bibliothèques dans calibre.

Une Bibliothèque virtuelle est différente d’une simple recherche. Une recherche restreindra uniquement la liste des livres affichés dans votre liste de livres. Une Bibliothèque virtuelle fait cela, et en complément elle restreint les entrées affichées dans le Navigateur d’étiquettes sur la gauche. Le Navigateur d’étiquettes affichera uniquement les étiquettes, auteurs, séries, éditeurs, etc.. qui proviennent des livres dans la Bibliothèque virtuelle. Une Bibliothèque virtuelle se comporte donc comme si la bibliothèque réelle contenait uniquement l’ensemble restreint de livres.

Création de Bibliothèques virtuelles

Pour utiliser une Bibliothèque virtuelle cliquez sur le bouton Bibliothèque virtuelle localisé à la gauche de la Barre de recherche et sélectionnez l’option Créer une Bibliothèque virtuelle. Comme premier exemple, créons une Bibliothèque virtuelle qui nous affiche uniquement les livres d’un auteur particulier. Cliquez le lien :guilabel:`Auteurs`comme montré dans l’image ci-dessous et choisissez l’auteur que vous voulez utiliser et cliquez OK.

La boîte de dialogue Créer une Bibliothèque Virtuelle a été remplie pour vous. Cliquez OK et vous verrez qu’une nouvelle Bibliothèque virtuelle a été créée, et que vous y avez été automatiquement redirigé, celle-ci affiche uniquement les livres de l’auteur sélectionné. En ce qui concerne calibre, c’est comme si votre bibliothèque contenait seulement les livres de l’auteur sélectionné.

Vous pouvez revenir à la bibliothèque complète à n’importe quel moment en cliquant encore une fois sur Bibliothèque virtuelle et en sélectionnant l’entrée nommée <None>.

Les Bibliothèques virtuelles sont basées sur des recherches. Vous pouvez utilisez n’importe quelle recherche comme base à une Bibliothèque virtuelle. La Bibliothèque virtuelle contiendra uniquement les livres assortis à la recherche. Tout d’abord, entrez la recherche que vous voulez utiliser dans la Barre de recherche ou construisez une recherche en utilisant le Navigateur d’étiquettes. Quand vous êtes satisfait des résultats renvoyés, cliquez le bouton Bibliothèque virtuelle, choisissez guilabel:Créer une Bibliothèque Virtuelle et entrez un nom pour la nouvelle Bibliothèque virtuelle. La Bibliothèque virtuelle sera alors créée sur base de la recherche. Les recherches sont très puissantes, pour des exemples de ce que vous pouvez faire avec celles-ci, voir L’interface de recherche.

Exemples de Bibliothèques virtuelles utiles

- Les livres ajoutés à calibre au cours du dernier jour:

date:>1daysago

- Les livres ajoutés à calibre au cours du dernier mois:

date:>30daysago

- Les livres avec une notation de 5 étoiles:

rating:5

- Les livres avec une notation d’au moins 4 étoiles:

rating:>=4

- Les livres sans notation:

rating:false

- Les périodiques téléchargés par la fonction Récupérer des Actualités dans calibre:

tags:=News and author:=calibre

- Les livres sans étiquettes:

tags:false

- Les livres sans couvertures:

cover:false

Travailler avec des Bibliothèques virtuelles

Vous pouvez éditer une Bibliothèque virtuelle précédemment créée ou la supprimer, en cliquant Bibliothèque virtuelle et en choisissant l’action appropriée.

Vous pouvez dire à calibre que vous voulez toujours appliquer une bibliothèque virtuelle particulière quand la bibliothèque habituelle est ouverte, en allant dans Préférences → Interface → Comportement.

Vous pouvez rapidement utilisez la recherche actuelle comme une Bibliothèque virtuelle temporaire en cliquant le bouton Bibliothèque virtuelle et en choisissant l’entrée *recherche actuelle

Vous pouvez afficher toutes les Bibliothèques virtuelles disponibles comme des onglets au dessus de la liste de livres. C’est particulièrement pratique si vous aimez très souvent basculer entre les bibliothèques virtuelles. Cliquez sur le bouton Bibliothèque virtuelle et sélectionnez Afficher les Bibliothèques virtuelles sous forme d’onglets. Vous pouvez réarranger les onglets par glisser et déposer et fermer ceux que vous ne voulez pas voir. Les onglets fermés peuvent être restaurés en faisant un clic droit sur sur la barre des onglets.

Utilisation des Bibliothèques virtuelles dans les recherches

Vous pouvez rechercher des livres qui sont dans une Bibliothèque virtuelle en utilisant le préfixe vl:. Par exemple, vl:Lu trouvera tous les livres dans la Bibliothèque virtuelle Lu. La recherche vl:Lu and vl:"Science Fiction" trouvera tous les livres qui sont tant dans les Bibliothèques virtuelles Lu que Science Fiction.

La valeur suivant vl: doit être le nom d’une Bibliothèque virtuelle. Si le nom de la Bibliothèque virtuelle contient des espaces entourez-le alors avec des guillemets.

Une utilisation d’une recherche Bibliothèque virtuelle se trouve dans le Serveur de contenu. Dans Préférences → Partager sur le net → Exiger nom d’utilisateur et mot de passe vous pouvez limiter les bibliothèques calibre visibles pour un utilisateur. Pour chaque bibliothèque visible vous pouvez spécifier une expression de recherche pour limiter par après les livres qui seront vus. Utilisez vl:"Nom Bibliothèque virtuelle" pour limiter les livres à ceux dans une Bibliothèque virtuelle.

Utilisation des restrictions supplémentaires

Vous pouvez limiter les livres montrés dans une Bibliothèque virtuelle à l’aide de :guilabel: Restrictions supplémentaires. Une restriction supplémentaire est recherche enregistrée que vous avez précédemment créée qui peut être appliqué à la Bibliothèque virtuelle actuelle pour limiter les livres montrés dans une bibliothèque virtuelle. Par exemple, disons que vous avez une Bibliothèque virtuelle pour des livres étiquetés comme Fiction Historique et une recherche enregistrée qui vous montre les livres non lus, vous pouvez cliquer sur le bouton Bibliothèque virtuelle et choisir Restrictions supplémentaires pour montrer uniquement les livres de fiction historique non lus. Pour se renseigner sur les recherches enregistrées, voir Recherches sauvegardées.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Le système d’URL calibre://

Le système d’URL calibre://
calibre s’enregistre en tant que programme de gestion des URL de calibre://. Vous pouvez donc les utiliser pour effectuer des actions telles que l’ouverture de livres, la recherche de livres, etc. à partir d’autres programmes/documents ou via la ligne de commande. Par exemple, en exécutant ce qui suit sur la ligne de commande

calibre calibre://switch-library/Some_Library

S’ouvrira dans calibre avec la bibliothèque nommée Une certaine bibliothèque. Les noms de bibliothèque sont le nom du dossier de la bibliothèque avec des espaces remplacés par des traits de soulignement. La valeur spéciale _ signifie la bibliothèque actuelle. Les différents types d’URL sont documentés ci-dessous.

Vous pouvez même placer ces liens à l’intérieur de fichiers HTML ou de documents Word ou similaires et le système d’exploitation exécutera automatiquement calibre pour effectuer l’action spécifiée.

- Passer à une bibliothèque spécifique

- Montrer un livre spécifique dans calibre

- Ouvrir un livre spécifique dans la visionneuse de livre numérique à un emplacement spécifique

- Recherche des livres

- Ouvrir une fenêtre de détails du livre pour un livre dans une bibliothèque

- Open the notes associated with an author/series/etc.

- Codage hexadécimal des paramètres d’URL

Passer à une bibliothèque spécifique

La syntaxe de l’URL est

calibre://switch-library/Library_Name

Les noms des bibliothèques sont le nom du dossier de la bibliothèque avec des espaces remplacés par des traits de soulignement. La valeur spéciale _ signifie la bibliothèque actuelle. Vous pouvez aussi utiliser :ref:`hex encoding ` pour les noms de bibliothèques, utile si les noms de bibliothèques ont des caractères spéciaux qui nécessiteraient un autre encodage d’URL. Les noms de bibliothèques codés en hexadécimal ressemblent à

_hex_-AD23F4BC

Où la partie après le préfixe _hex_- est le nom de la bibliothèque encodé en UTF-8 et chaque octet représenté par deux caractères hexadécimaux.

Montrer un livre spécifique dans calibre

La syntaxe de l’URL est

calibre://show-book/Library_Name/book_id

Cela montrera le livre avec book_id (un numéro) dans calibre. Les identifiants des livres peuvent être vus dans l’interface de calibre en passant le curseur sur le lien Cliquer pour ouvrir dans le panneau Détails du livre, c’est le numéro entre parenthèses à la fin du chemin d’accès au dossier du livre.

Vous pouvez copier un lien vers le livre actuel affiché dans calibre en faisant un clic droit sur le panneau Détails du livre et en choisissant Copier le lien vers le livre.

Si une recherche est active et que le livre ne correspond pas à la recherche, la recherche est annulée.

Si une bibliothèque virtuelle est sélectionnée, calibre l’utilisera pour afficher le livre. Si le livre n’est pas trouvé dans cette bibliothèque virtuelle, celle-ci est effacée.

Si vous souhaitez passer à une bibliothèque virtuelle particulière lorsque vous montrez le livre, utilisez :

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

en remplaçant les espaces dans le nom de la bibliothèque virtuelle par %20. Si le livre ne se trouve pas dans cette bibliothèque virtuelle, la bibliothèque virtuelle est ignoré.

Ouvrir un livre spécifique dans la visionneuse de livre numérique à un emplacement spécifique

La syntaxe de l’URL est

calibre://view-book/Library_Name/book_id/book_format?open_at=location

Ici, book_format est le format du livre, par exemple, EPUB ou MOBI et location est un emplacement optionnel à l’intérieur du livre. La façon la plus simple d’obtenir ces liens est d’ouvrir un livre dans la visionneuse, puis dans les commandes de la visionneuse, sélectionnez Aller à → Emplacement et là un tel lien sera donné que vous pourrez copier/coller ailleurs.

Recherche des livres

La syntaxe de l’URL est

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

Ici, toute requête est valide expression de recherche. Si l’expression de recherche est compliquée, code la comme une chaîne hexadécimale et utilise eq à la place. L’omission de la requête entraînera l’annulation de la recherche en cours.

Par défaut, si une Bibliothèque virtuelle est sélectionnée, calibre l’effacera avant de faire la recherche pour s’assurer que tous les livres sont trouvés. Si vous souhaitez préserver la bibliothèque virtuelle, utilisez

calibre://search/Library_Name?q=query&virtual_library=_

Si vous souhaitez passer à une Bibliothèque virtuelle particulière, utilisez

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

en remplaçant les espaces dans le nom de la bibliothèque virtuelle par %20.

Si vous effectuez une recherche dans calibre et souhaitez générer un lien pour celle-ci, vous pouvez le faire en cliquant avec le bouton droit de la souris sur la barre de recherche et en choisissant Copier la recherche en tant qu’URL.

Ouvrir une fenêtre de détails du livre pour un livre dans une bibliothèque

La syntaxe de l’URL est

calibre://book-details/Library_Name/book_id

Cette opération permet d’ouvrir une fenêtre de détails du livre spécifié dans la bibliothèque spécifiée sans modifier la bibliothèque actuelle ou le livre sélectionné.

Open the notes associated with an author/series/etc.

La syntaxe de l’URL est

calibre://book-details/Library_Name/Field_Name/id_Item_Id

This opens a window showing the notes of the specified item.
The easiest way to create such URLs is to show the notes you want in calibre and click the Copy URL button to copy the URL to the clipboard and paste it wherever you need.

Here Field_Name is the name of the columns such as authors or tags.
For user created columns, replace the leading # in the field name with an underscore, so #mytags becomes _mytags.

In addition to specifying items by id using Item_Id you can also specify them by name using either val_Item_Name or hex_Hex_Encoded_Item_Name.
For example:

calibre://book-details/Library_Name/authors/val_John%20Doe

Codage hexadécimal des paramètres d’URL

Le codage hexadécimal des paramètres d’URL se fait en codant d’abord le paramètre en UTF-8 octets, puis en remplaçant chaque octet par deux caractères hexadécimaux représentant l’octet. Par exemple, la chaîne abc correspond aux octets 0x61 0x62 et 0x63 en UTF-8, donc la version codée est la chaîne : 616263.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Personnaliser calibre

Personnaliser calibre
calibre a une conception hautement modulaire. Plusieurs de ses parties peuvent être personnalisées. Ici, vous apprendrez :

- comment utiliser les variables d’environnement et les ajustements pour personnaliser le comportement de calibre,

- comment spécifier vos propres ressources statiques, comme les icônes et les modèles, pour remplacer les valeurs par défaut

- comment utiliser les plugins pour ajouter des fonctionnalités à calibre.

- comment partager des thèmes d’icônes et des plugins avec d’autres utilisateurs de calibre.

- pour voir comment créer des recettes pour ajouter de nouvelles sources de contenu en ligne à calibre, visitez la Section Ajouter votre site web favori d’actualités.

Note

Notez que bien que les thèmes d’icônes et les plugins soient indexés et téléchargeables via le dispositif de mise à jour intégré de calibre, ils ne font pas partie de calibre, et leurs emplacements canoniques pour le support et le code source sont sur les forums Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] dans leurs fils de discussion de support.

- Variables d’environnement

- Ajustements

- Outrepasser les Icônes, les modèles, etc.

- Créer votre propre thème d’icônes pour calibre

- Personnalisation de calibre avec des extensions

Variables d’environnement

- CALIBRE_CONFIG_DIRECTORY - paramètre le dossier où les fichiers de configuration sont stockés/lus.

- CALIBRE_TEMP_DIR - paramètre le dossier temporaire utilisé par calibre

- CALIBRE_CACHE_DIRECTORY - défini le dossier utilisé pour un cache persistant des données entre les sessions

- CALIBRE_OVERRIDE_DATABASE_PATH - vous permet de spécifier le chemin de fichier complet vers metadata.db. En utilisant cette variable vous pouvez avoir metadata.db dans un autre emplacement que le répertoire de la bibliothèque. Utile si votre répertoire de bibliothèque est sur un lecteur réseau qui ne supporte pas le verrouillage de fichier.

- CALIBRE_DEVELOP_FROM - utilisé pour exécuter à partir d’un environnement de développement calibre. Voir Mise en place d’un environnement de développement calibre.

- CALIBRE_OVERRIDE_LANG - utilisé pour forcer la langue utilisée par l’interface(code de langue ISO 639)

- CALIBRE_TEST_TRANSLATION - utilisé pour tester une traduction d’un fichier .po (devrait être le chemin vers le fichier .po)

- CALIBRE_NO_NATIVE_FILEDIALOGS - force calibre à ne pas se servir des fichiers de dialogues natifs pour la sélection des fichiers/dossiers.

- CALIBRE_NO_NATIVE_MENUBAR - entraîne le fait que calibre ne crée pas de menu natif (global) sur Ubuntu Unity et les environnements de bureau Linux similaires. A la place, le menu est placé dans la fenêtre, comme d’habitude.

- CALIBRE_USE_SYSTEM_THEME - par défaut, sous Linux, calibre utilise son propre style Qt intégré. Ceci pour éviter des plantages et des blocages par incompatibilités entre la version Qt sur laquelle est basée calibre et la version Qt du système. La contrepartie est que calibre ne suit pas l’apparence du système. Si vous paramétrez cette variable sous Linux, cela entraînera que calibre utilisera le thème du système – prendre garde aux plantages et blocages.

- CALIBRE_SHOW_DEPRECATION_WARNINGS - entraîne l’impression par calibre d’avertissements de dépréciation vers stdout. Utile pour les développeurs calibre.

- CALIBRE_NO_DEFAULT_PROGRAMS - empêche calibre d’enregistrer automatiquement les types de fichiers qu’il est capable de gérer sous Windows.

- QT_QPA_PLATFORM - Sous Linux, définissez ce paramètre à wayland` pour forcer calibre à utiliser Wayland et à xcb pour forcer l’utilisation de X11.

- SYSFS_PATH - Utiliser si sysfs est monté autre part que /sys

- http_proxy, https_proxy - utilisés sous Linux pour spécifier un proxy HTTP(S)

Voir Comment définir les variables d’environnement dans Windows [https://www.computerhope.com/issues/ch000549.htm]. Si vous êtes sous macOS, vous pouvez définir des variables d’environnement en créant le fichier ~/Library/Preferences/calibre/macos-env.txt et en y mettant les variables d’environnement une par ligne, par exemple

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Ajustements

Les ajustements sont de petits changements que vous pouvez spécifier pour contrôler divers aspects du comportement de calibre. Vous pouvez les changer en allant dans Préférences->Avancé->Ajustements. Les valeurs par défaut pour les ajustements sont reproduits ci-dessous

#!/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'

Outrepasser les Icônes, les modèles, etc.

Note
calibre a un support direct pour les thèmes d’icônes, il y a plusieurs thèmes d’icônes disponibles pour calibre, que vous pouvez utiliser en allant dans Préférences → Interface → Apparence et présentation → Changer le thème d’Icône. Il est préférable d’utiliser les thèmes d’icônes plutôt que de remplacer les icônes individuelles.

calibre vous permet d’outrepasser les ressources statiques, comme les icônes, le Javascript et les modèles pour les jaquettes de métadonnées, catalogues, etc. avec des versions personnalisées que vous préférez. Toutes les ressources statiques sont stockées dans le sous-répertoire ressources de l’emplacement d’installation de calibre. Sous Windows c’est habituellement C:\Program Files\app\Calibre2\resources. Sous macOS, /Applications/calibre.app/Contents/Resources/resources/. Sous Linux, si vous utilisez l’installateur de binaires depuis le site web de calibre ce sera /opt/calibre/resources. Ces chemins peuvent changer dépendant de où vous choisissez d’installer calibre.

Vous ne devriez pas changer les fichiers de ce répertoire ressources, vu que vos changements seront écrasés lors de votre prochaine mise à jour de calibre. A la place, allez dans Préférences → Avancé → Divers et cliquez Ouvrir le répertoire de configuration de calibre. Dans ce répertoire de configuration, créer un sous dossier appelé resources et placez y les fichiers que vous voulez remplacer. Placer les fichiers dans le sous dossier approprié, par exemple placez les images dans resources/images, etc. calibre utilisera automatiquement votre fichier personnalisé en préférence à celui intégré la prochaine fois qu’il est démarré.

Par exemple, si vous désirez changer l’icône pour l’action Supprimer des livres, vous devrez d’abord regarder dans le répertoire resources intégré et voir que le ficher pertinent est resources/images/remove_books.png. Supposons que vous avez une icône alternative appelée my_remove_books.png vous devriez la sauver dans le dossier de configuration comme file:resources/images/remove_books.png. Toutes les icônes de l’interface utilisateur calibre sont dans resources/images et ses sous.répertoires. Placer un fichier de remplacement ici aura une priorité encore plus élevée qu’un thème d’icône personnalisé.

Créer votre propre thème d’icônes pour calibre

Si vous avez créé un superbe ensemble d’icônes et que vous souhaitez le partager avec les autres utilisateurs de calibre via le support de thème intégré, vous pouvez facilement les empaqueter dans un thème. Pour ce faire, aller à Préférences → Divers → Créer un thème d’icônes, sélectionner le dossier où vous avez posé vos icônes. Entrez alors les métadonnées du thème et cliquez sur OK. Ceci créera un fichier ZIP contenant vos icônes à thème. Vous pouvez téléverser cela sur le forum calibre à l’adresse Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] et je rendrai votre thème disponible via le système de thème d’icônes intégré de calibre. Par défaut, le thème d’icônes que vous venez de créer sera également installé comme thème courant dans calibre. Si vous testez votre thème, pensez à supprimer les images du dossier resources/images afin que les icônes du thème soient utilisées.

A partir de calibre 6, vous pouvez avoir des icônes personnalisées pour le mode clair et le mode sombre. Il suffit de créer deux versions de l’icône et de nommer les fichiers avec le suffixe -for-dark-theme et -for-light-theme`'. Par exemple, ``modified-for-dark-theme.png et modified-for-light-theme.png. calibre utilisera alors automatiquement l’icône appropriée en fonction du thème actuel.

Personnalisation de calibre avec des extensions
calibre a un design hautement modulaire. Presque toutes les fonctionnalités dans calibre viennent sous la forme d’extensions. Les extensions sont utilisées pour la conversion, pour télécharger des actualités (bien que celles-ci s’appellent des recettes), pour divers composants de l’interface utilisateur, pour connecter différents périphériques, pour traiter les fichiers lorsque vous les ajouter à calibre. Vous pouvez obtenir une liste complète de toutes les extensions intégrées dans calibre en allant à Préférences → Avancé → Extensions.

Vous pouvez écrire vos propres extensions pour personnaliser et améliorer le coportement de calibre. L’architecture extension dans calibre est vraiment simple, voir le tutoriel Écrire vos propres extensions pour étendre les fonctionnalités de calibre.

Une fois que vous avez écrit un plugin, vous pouvez le télécharger sur le forum des plugins de calibre à l’adresse Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237] et il sera mis à disposition via le dispositif de mise à jour des plugins intégré à calibre.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Personnaliser calibre »
- Documentation API pour les extensions

Documentation API pour les extensions

Définit différentes classes abstraites de base qui peuvent être sous-classées pour créer de puissantes extensions. Les classes utiles sont :

- Extension

- FileTypePlugin

- Les extensions métadonnées

- Extension de catalogue

- Extension de téléchargement de métadonnées

- Extension de conversion

- Pilotes de périphérique

- Actions interface utilisateur

- Extensions préférences

Extension
class calibre.customize.Plugin(plugin_path)[source]

Une extension calibre. Les membres utiles incluent:

- self.installation_type : Enregistre la façon dont le plugin a été installé.

- self.plugin_path : Enregistre le chemin vers le fichier ZIP le contenant

cette extension ou None si c’est une extension intégrée

- self.site_customization: Enregistre une chaîne de personnalisation introduite

par l’utilisateur.

Les méthodes qui devraient être annulées dans les sous-classes :

- initialize()

- customization_help()

Méthodes utiles :

- temporary_file()

- __enter__()

- load_resources()

supported_platforms = []

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

Le nom de cette extension. Vous devez le fixer à autre chose que Trivial Plugin pour que cela fonctionne.

version = (1, 0, 0)

La version de cette extension en tant que 3-tuple (majeure, mineure, révision)

description = 'Ne fait absolument rien'

Une courte chaine décrivant ce que fait l’extension

author = 'Inconnu(e)'

L’auteur de cette extension

priority = 1

Quand plus d’une extension existe pour un type de fichier, les extensions sont exécutées dans l’ordre de priorité décroissant. Les extensions avec la priorité la plus haute seront exécutées en premier. La priorité la plus haute possible est sys.maxsize. La priorité par défaut est de 1.

minimum_calibre_version = (0, 4, 118)

La version la plus récente de calibre que nécessite cette extension

installation_type = None

La façon dont ce plugin est installé

can_be_disabled = True

Si False, l’utilisateur ne pourra pas désactiver cette extension. À utiliser avec soin.

type = 'Base'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

initialize()[source]

Appelé une fois lorsque les extensions calibre sont initialisées. Les extensions sont ré-initialisées à chaque fois qu’une nouvelle extension est ajoutée. Notez également que si l’extension est en exécution dans un processus de travail, tel que pour ajouter des livres, alors l’extension sera initialisée pour tout nouveau processus de travail.

Effectuer n’importe quel initialisation spécifique d’extension ici, comme l’extraction de ressource à partir du fichier ZIP de l’extension. Le chemin vers le ZIP est disponible comme self.plugin_path.

Notez que self.site_customization n’est pas disponible à ce point-ci.

config_widget()[source]

Implémenter cette méthode et save_settings() dans votre extension pour utiliser une boîte de dialogue de configuration personnalisée, plutôt que d’utiliser la simple personnalisation par ligne par défaut.

Cette méthode, si implémentée, doit retourner un QWidget. Le widget peut avoir une méthode optionnelle validate() qui ne prend pas d’argument et qui est appelée dès que l’utilisateur clique sur OK. Les changements sont appliqués si et seulement si la méthode retourne True.

Si pour une raison quelconque, vous ne pouvez pas réaliser la configuration à ce moment, renvoie un tuple de deux chaines (message, détails), qui seront affichés comme un dialogue d’alerte à l’utilisateur et le processus sera interrompu.

save_settings(config_widget)[source]

Sauve les paramètres spécifiés par l’utilisateur avec config_widget

Paramètres:

config_widget – Le widget renvoyé par config_widget().

do_user_config(parent=None)[source]

Cette méthode montre une boîte de dialogue de configuration pour cette extension. Elle renvoie True si l’utilisateur clique OK, sinon False. Les changements sont appliqués automatiquement.

load_resources(names)[source]

Si cette extension est donnée dans un fichier ZIP (extension ajoutée par l’utilisateur), cette méthode vous permettra de charger des ressources du fichier ZIP.

Par exemple pour charger une image

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

Paramètres:

names – Liste de chemins vers les ressources dans le ZIP utilisant / comme séparateur

Renvoie:

Un dictionnaire sous la forme {name : file_contents}. Tout nom qui n’est pas trouvé dans le fichier ZIP ne sera pas présent dans le dictionnaire.

customization_help(gui=False)[source]

Retourne une chaîne offrant de l’aide sur comment personnalisé cette extension. Par défaut avance un NotImplementedError, qui indique que l’extension ne nécessite pas de personnalisation.

Si vous ré-implémentez cette méthode dans votre sous-classe, il sera demandé à l’utilisateur d’entrer une chaîne pour personnalisé cette extension. La chaîne de personnalisation sera disponible comme self.site_customization.

La personnalisation de site peut-être n’importe quoi, par exemple, le chemin vers un binaire nécessaire sur l’ordinateur de l’utilisateur.

Paramètres:

gui – Si True renvoie l’aide HTML, sinon renvoie l’aide en texte simple.

temporary_file(suffix)[source]

Renvoie un objet ressemblant à un fichier qui est un fichier temporaire dans les fichiers systèmes. Ce fichier reste disponible après avoir été fermé et sera supprimé uniquement après l’extinction de l’interpréteur. Utilisé le membre name de l’objet renvoyé pour accéder au chemin complet vers le fichier temporaire créé.

Paramètres:

suffix – Le suffixe qu’aura le fichier temporaire.

cli_main(args)[source]

Cette méthode est le point d’entrée principal pour les interfaces d’extensions en ligne de commande. Elle est appelée lorsque l’utilisateur fait: calibre-debug -r « Nom de l’Extension ». Tous les arguments sont présents dans les variables args.

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[source]

Bases : Plugin

Une extension qui est associée à un ensemble particulier de types de fichiers.

file_types = {}

Ensemble de types de fichiers pour lesquels cette extension devrait fonctionner. Utilisez “*” pour tous les types de fichiers. Par exemple : {'lit', 'mobi', 'prc'}

on_import = False

Si True, cette extension est exécutée lorsque des livres sont ajoutés à la base de donnée

on_postimport = False

Si réglé sur True, cette extension est exécutée après que des livres aient été ajoutés à la base de donnée. Dans ce cas, les méthodes postimport et postadd de l’extension sont appelées.

on_postconvert = False

Si True, ce plugin est exécuté après la conversion d’un livre. Dans ce cas, la méthode postconvert du plugin est appelée.

on_postdelete = False

Si True, ce plugin est exécuté après qu’un fichier de livre a été supprimé de la base de données. Dans ce cas, la méthode postdelete du plugin est appelée.

on_preprocess = False

Si True, cette extension est exécutée juste avant une conversion

on_postprocess = False

Si True, cette extension est exécutée après conversion sur le fichier final produit par l’extension de conversion de sortie.

type = 'Type de fichier'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

run(path_to_ebook)[source]

Exécute l’extension. Doit être implémenté dans les sous-classes. Cela devrait effectuer n’importe quelles modifications requises sur le livre numérique et renvoyer le chemin absolu vers le livre numérique modifié. Si aucune modifications ne sont nécessaire, cela devrait renvoyer le chemin vers le livre numérique original. Si une erreur est rencontrée, cela devrait amener une Exception. L’implémentation par défaut retourne simplement le chemin vers le livre numérique original. Notez que le chemin vers le fichier original (avant que n’importe quelle extension type de fichier ne soit exécutée, est disponible comme ceci self.original_path_to_file).

Le livre numérique modifié devrait être créer avec la méthode temporary_file().

Paramètres:

path_to_ebook – Chemin absolu vers le livre numérique.

Renvoie:

Chemin absolu vers le livre numérique modifié.

postimport(book_id, book_format, db)[source]

post import est appelée, c-à-d après que le fichier de livre ait été ajouté à la base de donnée. Notez que ceci est différent de postadd() qui est appelée lorsque les données du livre sont créées pour la première fois. Cette méthode est appelée à chaque fois qu’un nouveau fichier est ajouté à un livre enregistré. Elle est utile pour modifier les données du livre se basant sur le contenu du fichier nouvellement ajouté.

Paramètres:

- book_id – L’id du livre ajouté dans la base de donnée.

- book_format – Le type de fichier du livre qui a été ajouté.

- db – Base de donnée de la bibliothèque.

postconvert(book_id, book_format, db)[source]

Appelé après la conversion, c’est-à-dire après que le fichier de sortie de la conversion a été ajouté à la base de données. Notez qu’elle n’est exécutée qu’après une conversion, et non après l’ajout d’un livre. Elle est utile pour modifier l’enregistrement du livre en fonction du contenu du fichier nouvellement ajouté.

Paramètres:

- book_id – L’id du livre ajouté dans la base de donnée.

- book_format – Le type de fichier du livre qui a été ajouté.

- db – Base de donnée de la bibliothèque.

postdelete(book_id, book_format, db)[source]

Appelé après la suppression, c’est-à-dire après que le fichier du livre a été supprimé de la base de données. Notez qu’il n’est pas exécuté lorsqu’un enregistrement de livre est supprimé, mais seulement lorsqu’un ou plusieurs formats du livre sont supprimés. Elle est utile pour modifier l’enregistrement du livre en fonction du format du fichier supprimé.

Paramètres:

- book_id – L’id du livre ajouté dans la base de donnée.

- book_format – Le type de fichier du livre qui a été ajouté.

- db – Base de donnée de la bibliothèque.

postadd(book_id, fmt_map, db)[source]

post add est appelée, c-à-d après qu’un livre ait été ajouté à la base de donnée. Notez que ceci est différent de postimport(), qui est appelée lorsque un fichier de livre est ajouté à un livre existant. postadd() est appelée uniquement lorsqu’un dossier entier de livre avec éventuellement plus d’un fichier de livre a été créé pour la première fois. Ceci est utile si vous souhaitez modifier les données d’un livre dans la base de donnée lorsque le livre est initialement ajouté à calibre.

Paramètres:

- book_id – L’id du livre ajouté dans la base de donnée.

- fmt_map – Le chemin de l’emplacement du format du fichier à partir duquel le format a été ajouté. Notez que ceci pourrait ne pas indiquer un fichier existant, vu que parfois certains fichier sont ajoutés en tant que flux. Dans ce cas on pourrait avoir une valeur fausse ou un chemin inexistant.

- db – Base de donnée de la bibliothèque

Les extensions métadonnées

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[source]

Bases : Plugin

Une extension qui implémente les métadonnées de lecture à partir d’un ensemble de types de fichier.

file_types = {}

Ensemble de types de fichier pour lesquels l’extension devrait être exécutée. Par exemple : set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

version = (7, 10, 0)

La version de cette extension en tant que 3-tuple (majeure, mineure, révision)

author = 'Kovid Goyal'

L’auteur de cette extension

type = 'Lecteur de métadonnées'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

get_metadata(stream, type)[source]

Renvoie les métadonnées pour le fichier représenté par le flux (un objet-fichier qui supporte la lecture). Renvoie une exception lorsqu’il y a une erreur avec les données d’entrée.

Paramètres:

type – Le type de fichier. Garanti d’être l’une des entrées dans file_types.

Renvoie:

Un objet calibre.ebooks.metadata.book.Metadata

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[source]

Bases : Plugin

Une extension qui implémente les métadonnées de lecture à partir d’un ensemble de types de fichier.

file_types = {}

Ensemble de types de fichier pour lesquels l’extension devrait être exécutée. Par exemple : set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

version = (7, 10, 0)

La version de cette extension en tant que 3-tuple (majeure, mineure, révision)

author = 'Kovid Goyal'

L’auteur de cette extension

type = 'Créateur de métadonnées'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

set_metadata(stream, mi, type)[source]

Règle les métadonnées pour le fichier représenté par le flux (un objet-fichier qui supporte la lecture). Renvoie une exception lorsqu’il y a une erreur avec les données d’entrée.

Paramètres:

- type – Le type de fichier. Garanti d’être l’une des entrées dans file_types.

- mi – Un objet calibre.ebooks.metadata.book.Metadata

Extension de catalogue

class calibre.customize.CatalogPlugin(plugin_path)[source]

Bases : Plugin

Une extension qui implémente un générateur de catalogue.

file_types = {}

Le type de fichier de sortie pour lesquels l’extension devrait être exécutée. Par exemple : “epub” ou “xml”

type = 'Générateur de catalogue'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

cli_options = []

Les options d’analyseur de CLI spécifiques pour cette extension, déclarée en tant qu”Option namedtuple:

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()[source]

Si l’extension n’est pas native, copier les fichiers .ui et .py de l’extension à partir du fichier ZIP vers $TMPDIR. Un entrée sera dynamiquement générée et ajotée à la boîte de dialogue du Catalogue d’Options dans calibre.gui2.dialogs.catalog.py:Catalog

run(path_to_output, opts, db, ids, notification=None)[source]

Exécute l’extension. Doit être implémenté dans les sous-classes. Cela devrait généré le catalogue dans le format spécifié dans file_types, renvoyant le chemin absolu vers le fichier catalogue généré. SI un erreur est rencontrée, cela devrait faire apparaître une Exception.

Le fichier catalogue généré devrait être créer avec la méthode temporary_file().

Paramètres:

- path_to_output – Chemin absolu vers le fichier catalogue généré.

- opts – Un dictionnaire d’arguments mot-clef

- db – Un objet BaseDeDonnéeLibrairie2

Extension de téléchargement de métadonnées
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[source]

Bases : Plugin

type = 'Source de métadonnées'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

author = 'Kovid Goyal'

L’auteur de cette extension

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

capabilities = frozenset({})

Ensemble de capacités supportées par cet extension. Les capacités utiles sont: “identify”, “cover”

touched_fields = frozenset({})

Liste de champ de métadonnée qui peuvent potentiellement être téléchargée par cette extension pendant la phase d’identification

has_html_comments = False

Fixer ceci à True si votre extension renvoie des commentaires formatés en HTML

supports_gzip_transfer_encoding = False

Paramétrer ceci à True veut dire que l’objet navigateur indiquera qu’il prend en charge l’encodage de transfert gzip. Ceci peu accélérer les téléchargements, soyez sûr que la source supporte effectivement les encodages de transfert gzip correctement.

ignore_ssl_errors = False

Paramétrer à TRUE pour ignorer les erreurs de certificat HTTPS lors de la connexion à cette source.

cached_cover_url_is_reliable = True

Des URLs de couverture dans le cache peuvent parfois être peu sûr (càd. que le téléchargement pourrait échouer ou que l’image pourrait ne pas fonctionner). Si c’est souvent le cas avec cette source, fixer à False

options = ()

Un liste d’objet Option. Ils seront utilisés pour construire automatiquement le widget de configuration pour cette extension

config_help_message = None

Une chaîne qui sera affichée au sommet du widget de configuration pour cette extension

can_get_multiple_covers = False

Si True cette source peut renvoyer de multiples couvertures pour une demande donnée

auto_trim_covers = False

Si fixer à True les couvertures téléchargées par cette extension seront automatiquement rognées.

prefer_results_with_isbn = True

Si réglé à True, et cette source retourne de multiples résultats pour une requête, certains de ceux-ci ont des ISBN et certains de ceux-ci n’en ont pas, le résultat sans ISBN sera ignoré.

is_configured()[source]

Renvoie False si votre extension nécessite d’être configurée avant de pouvoir être utilisé. Par exemple, cela pourrait nécessiter un utilisateur/mot de passe/clef de greffon.

customization_help()[source]

Retourne une chaîne offrant de l’aide sur comment personnalisé cette extension. Par défaut avance un NotImplementedError, qui indique que l’extension ne nécessite pas de personnalisation.

Si vous ré-implémentez cette méthode dans votre sous-classe, il sera demandé à l’utilisateur d’entrer une chaîne pour personnalisé cette extension. La chaîne de personnalisation sera disponible comme self.site_customization.

La personnalisation de site peut-être n’importe quoi, par exemple, le chemin vers un binaire nécessaire sur l’ordinateur de l’utilisateur.

Paramètres:

gui – Si True renvoie l’aide HTML, sinon renvoie l’aide en texte simple.

config_widget()[source]

Implémenter cette méthode et save_settings() dans votre extension pour utiliser une boîte de dialogue de configuration personnalisée, plutôt que d’utiliser la simple personnalisation par ligne par défaut.

Cette méthode, si implémentée, doit retourner un QWidget. Le widget peut avoir une méthode optionnelle validate() qui ne prend pas d’argument et qui est appelée dès que l’utilisateur clique sur OK. Les changements sont appliqués si et seulement si la méthode retourne True.

Si pour une raison quelconque, vous ne pouvez pas réaliser la configuration à ce moment, renvoie un tuple de deux chaines (message, détails), qui seront affichés comme un dialogue d’alerte à l’utilisateur et le processus sera interrompu.

save_settings(config_widget)[source]

Sauve les paramètres spécifiés par l’utilisateur avec config_widget

Paramètres:

config_widget – Le widget renvoyé par config_widget().

get_author_tokens(authors, only_first_author=True)[source]

Prend une liste d’auteurs et retourne une liste de jetons utiles pour une demande de recherche AND. Cette fonction tente de renvoyer des jetons dans l’ordre prénom, second nom, nom de famille, en escomptant que si une virgule est dans le nom d’auteur, le nom est dans le nom de famille, sous forme des autres noms.

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[source]

Prend un titre et retourne une liste de jetons utiles pour une requête de recherche AND. Exclus les conjonctions(optionnel) et la ponctuation.

split_jobs(jobs, num)[source]

Divise une liste de travaux en sous groupe, aussi uniformément que possible

test_fields(mi)[source]

Retourne le premier champs de self.touched_fields qui est nul sur le demi objet

clean_downloaded_metadata(mi)[source]

Appelez cette méthode dans votre méthode d’identification de votre extension pour normaliser les métadonnées avant de rentrer l’objet Métadonnées dans le result_queue. Vous pouvez, bien sûr, utiliser un algorithme personnalisé adapté pour la source de métadonnée.

get_book_url(identifiers)[source]

Renvoie un 3-tuple ou None. Le 3-tuple est sous la forme: (identifier_type, identifier_value, URL). L’URL est celle du livre identifié par les identifiants à la source. identifier_type, identifier_value, spécifiant l’identifiant correspondant à l’URL. Cet URL doit être consultable pour un être humain utilisant un navigateur. Elle est censée offrir un lien cliquable pour l’utilisateur pour visiter facilement les pages du livre à la source. Si aucun URL n’est trouvé, renvoie None. Cette méthode doit être rapide, et cohérente, à implémenter uniquement s’il est possible de construire l’URL à partir d’un schéma connu d’identifiants.

get_book_url_name(idtype, idval, url)[source]

Renvoie un nom lisible par un humain pour la valeur de retour de get_book_url()

get_book_urls(identifiers)[source]

Outrepassez cette méthode si vous souhaiteriez retourner de multiples URLs pour ce livre. Renvoie une liste de 3-tuples. Par défaut cette méthode appelle simplement get_book_url().

get_cached_cover_url(identifiers)[source]

Renvoie l’URL de la couverture mise en cache pour le livre identifié par le dictionnaire d’identifiants ou None si cette URL n’existe pas.

Noter que cette méthode doit uniquement renvoyer des URLs validées, càd. pas des URLs qui pourrait finir en une image de couverture générique ou une erreur « not found »

id_from_url(url)[source]

Analyser une URL et retourner un tuple du formulaire : (identifier_type, identifier_value). Si l’URL ne correspond pas au modèle de la source de métadonnées, renvoyer None.

identify_results_keygen(title=None, authors=None, identifiers={})[source]

Renvoie une fonction qui est utilisée pour générer une clef qui peut trier les objets Métadonnées par leur pertinence en fonction d’une recherche donnée (titre, auteurs, identifiants).

Ces clefs sont utilisées pour trier les résultats d’un appel de identify().

Pour plus de détails sur l’algorithme par défaut voir InternalMetadataCompareKeyGen. Ré-implémentez cette fonction dans votre extension si l’algorithme par défaut n’est pas adapté.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[source]

Identifie un livre par son Titre/Auteur/ISBN/etc.

Si des identifiants sont spécifiés et qu’aucune n’est trouvée et que cette source de métadonnée ne contient pas tous les identifiants demandé (par exemple, tous les ISBNs d’un livre), cette méthode devrait réessayer avec juste le titre et l’auteur (s’ils ont été spécifiés).

Si cette source de métadonnées fournit aussi des couvertures, l’URL de la couverture devrait être mis en cache pour qu’une demande suivant dans une API pour chercher les couvertures avec le même ISBN/identifiant spécial ne nécessite pas de rechercher l’URL de la couverture à nouveau. Utiliser le cache API pour ceci.

Chaque métadonnée introduite dans result_queue avec cette méthode doit avoir un attribut source_relevance qui est un entier indiquant l’ordre dans lequel les résultats seront rendus par la source de métadonnée pour cette recherche. Cet entier sera utilisé par compare_identify_results(). Si l’ordre n’a pas d’importance, fixer à zéro pour chaque résultat.

Assurez-vous que toutes les informations de mappage de couverture/ISBN sont mises en cache avant que l’objet Metadata ne soit placé dans result_queue.

Paramètres:

- log – Un fichier log, utiliser le pour produire des informations de débogage/d’erreurs

- result_queue – Une File d’Attente de résultats, les résultats devraient y être mis. Chaque résultat est une métadonnée

- abort – Si abort.is_set() renvoie True, annule les futurs exécutions et retourne le plus vite possible

- title – Le titre du livre, peut être None

- authors – Une liste d’auteur du livre, peut être None

- identifiers – Un ensemble d’autres identifiants, le plus souvent {“isbn”:”1234…”}

- timeout – « Timeout » en seconde, aucune requête réseau ne devrait durer plus longtemps que « timeout ».

Renvoie:

None si aucune erreur n’apparaît, sinon une représentation unicode de l’erreur adaptée pour le visionnage par l’utilisateur

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[source]

Télécharge une couverture et l’insère dans result_queue. Les paramètres auront tous le même sens que pour identify(). Insère (self, cover_data) dans result_queue.

Cette méthode devrait utiliser les URLs de couvertures mise en cache par efficience le plus souvent possible. Quand aucune donnée n’est présente en cache, la plupart des extensions appel simplement identify et utilise ces résultats.

Si le paramètre get_best_cover est True dans cette extension et que cette extension peut récupérer de multiples couvertures, elle ne devrait prendre que la « meilleure ».

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[source]

Génère une sorte de clef pour la comparaison de la pertinence des métadonnée, en fonction d’une recherche. Ceci est utilisé uniquement pour comparer des résultats d’une même source de métadonnées, pas à travers de différentes sources.

La clé de tri garanti que l’ordre croissant de tri soit trié par pertinence décroissante.

L’algorithme est:

- Préfère des résultats qui ont au moins un identificateur égal à celui de la requête

- Préfère des résultats avec une URL de couverture en cache

- Préfère des résultats avec tous les champs disponible remplis

- Préfère les résultats avec la même langue que la langue de l’interface utilisateur actuelle

- Préfère des résultats qui ont exactement le même titre que la recherche

- Préfère des résultats qui ont des plus longs commentaires (plus de 10% plus grand)

- Utilise la pertinence du résultat comme reporté dans la recherche de source de métadonnées

machine

Extension de conversion
class calibre.customize.conversion.InputFormatPlugin(*args)[source]

Bases : Plugin

Les InputFormatPlugins sont responsable de la conversion d’un document en HTML+OPF+CSS+etc. Le résultat de la conversion doit être encodé en UTF-8. L’action principale se passe dans convert().

type = 'Entrée de conversion'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

can_be_disabled = False

Si False, l’utilisateur ne pourra pas désactiver cette extension. À utiliser avec soin.

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

file_types = {}

Ensemble de type de fichier pour lesquels l’extension devrait être exécuté. Par exemple : set(['azw', 'mobi', 'prc'])

is_image_collection = False

Si True, cette extension d’origine génère un ensemble d’image, une par fichier HTML. Ceci peut-être fait dynamiquement, dans la méthode de conversion si les fichiers d’origines peuvent être des ensembles d’images ou pas. Si vous fixer ceci à True, vous devez implémenter la méthode get_images() qui renvoie une liste d’images.

core_usage = 1

Nombre de cœur du CPU utilisés par cette extension. Une valeur de -1 signifie qu’elle utilise tous les cœurs disponibles

for_viewer = False

Si fixer à True, cette extension d’origine effectuera un exécution spéciale pour que son résultat soit approprié au visionnage

output_encoding = 'utf-8'

L’encodage dans lequel cette extension d’origine crée le fichier. Une valeur de None signifie que l’encodage est indéfini et doit être détecter individuellement

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Les options partagées par toutes les extensions de Format d’Entrée. N’outrepasses pas en sous-classe. Utiliser options à la place. Chaque option doit être un exemple de OptionRecommendation.

options = {}

Des options pour personnaliser le comportement de cette extension. Chaque option doit être un exemple de OptionRecommendation.

recommendations = {}

Un ensemble de 3-tuples sous la forme (option_name, recommended_value, recommendation_level)

get_images()[source]

Renvoie une liste de chemin absolu vers les images, si cette extension d’origine représente une collection d’image. La liste d’images est dans le même ordre que le manifeste et la TdM

convert(stream, options, file_ext, log, accelerators)[source]

Cette méthode doit être implémentée en sous-classe. Elle doit renvoyer le chemin vers le fichier OPF créé ou un cas OEBBook. Tous les fichiers sortant devrait être contenu dans ce dossier. Si cette extension crée des fichiers en dehors du dossier ils doivent être supprimés/marqués pour suppression avant le résultat de cette méthode.

Paramètres:

- stream – Un objet ressemblant à un fichier qui contient le fichier original.

- options – Des options pour personnalisés le processus de conversion. Garanti d’avoir des attributs correspondant à toutes les options déclarée dans cette extension. De plus, il y aura un attribut « verbose »(bavard) qui prend des valeurs entières à partir de zéro. Plus le nombre est haut, plus l’extension est bavarde. Un autre attribut utile est input_profile qui est un exemple de calibre.customize.profiles.InputProfile.

- file_ext – L’extension (sans le .) du fichier original. Garanti d’être un des file_types supporté par cette extension.

- log – Un objet calibre.utils.logging.Log. Tous les fichiers de sorties devraient utiliser cet objet.

- accelarators – Un dictionnaire d’informations variées que l’extension d’origine peut accéder facilement pour accélérer les étapes suivantes de la conversion.

postprocess_book(oeb, opts, log)[source]

Appelé pour permettre l’extension d’origine d’effectuer du post-traitement après que le livre ait été divisé.

specialize(oeb, opts, log, output_fmt)[source]

Appelé pour permettre à l’extension d’origine pour spécialiser le livre divisé pour un format de sortie particulier. Appelé après postprocess_book et avant toutes transformations effectuée sur le livre divisé.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Appelé pour créer un widget utilisé pour configurer cette extension dans le GUI de calibre. Le widget doit être une instance de la classe PluginWidget. Voir les d’extensions implémentées d’origine pour des exemples.

class calibre.customize.conversion.OutputFormatPlugin(*args)[source]

Bases : Plugin

Les OutputFormatPlugins sont responsables de la conversion d’un document OEB (OPF+HTML) vers le livre numérique de sortie.

Le document OEB peut être supposé être encodé en UTF-8. L’action principale se passe dans conversion().

type = 'Sortie de conversion'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

can_be_disabled = False

Si False, l’utilisateur ne pourra pas désactiver cette extension. À utiliser avec soin.

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

file_type = None

Le type de fichier (l’extension sans le point) que cette extension produit

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Les options partagées par toutes les extensions de Format d’Entrée. N’outrepasses pas en sous-classe. Utiliser options à la place. Chaque option doit être un exemple de OptionRecommendation.

options = {}

Des options pour personnaliser le comportement de cette extension. Chaque option doit être un exemple de OptionRecommendation.

recommendations = {}

Un ensemble de 3-tuples sous la forme (option_name, recommended_value, recommendation_level)

property description

str(object=””) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Créez un nouvel objet chaîne à partir de l’objet donné. Si un encodage ou des erreurs sont spécifiés, l’objet doit alors exposer un tampon de données qui sera décodé en utilisant l’encodage et le gestionnaire d’erreurs donnés. Sinon, renvoie le résultat de object.__str__() (si défini) ou repr(object). encoding default to sys.getdefaultencoding(). errors default to “strict”.

convert(oeb_book, output, input_plugin, opts, log)[source]

Fournit le contenu de oeb_book (qui est une instance de calibre.ebooks.oeb.OEBBook) au fichier spécifié par la sortie.

Paramètres:

- output – Soit un fichier soit une chaîne. Si c’est une chaîne, c’est le chemin vers un dossier qui peut exister ou pas. L’extension de sortie devrait écrire ses restitutions dans ce dossier. Si c’est un fichier, l’extension de sortie devrait écrire ces productions dans le fichier.

- input_plugin – L’extension d’origine qui a été utilisé au début de la pipeline de conversion.

- opts – Des options de conversions. Garantie d’avoir des attributs correspondant aux OptionRecommendations de cette extension.

- log – L’enregistreur. Sauver des messages d’info/débogage etc. en utilisant ceci.

specialize_options(log, opts, input_fmt)[source]

Peut être utilisé pour modifier les valeurs des options de conversion, telles qu’elles sont utilisées par le pipeline de conversion.

specialize_css_for_output(log, opts, item, stylizer)[source]

Peut-être utilisé pour effectuer des changements dans le CSS durant le processus d’aplanissement CSS.

Paramètres:

- item – L’objet (fichier HTML) étant traité

- stylizer – Un objet Styliste contenant les styles aplani pour l’objet. Vous pouvez obtenir les styles pour n’importe quel élément par stylizer.style(élément).

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Appelé pour créer un widget utilisé pour configurer cette extension dans le GUI de calibre. Le widget doit être une manipulation de la classe PluginWidget. Voir la création d’extensions d’origines pour des exemples.

Pilotes de périphérique

La classe de base pour tous les pilotes de périphérique est DevicePlugin. Cependant, si votre périphérique expose un port USBMS au système opérateur, vous devriez utiliser la classe USBMS à la place car elle implémente toute la logique nécessaire pour supporter ce genre de périphérique.

class calibre.devices.interface.DevicePlugin(plugin_path)[source]

Bases : Plugin

Définit l’interface qui devrait être implémentée par les derniers processus qui communiquent avec le lecteur de livre numérique.

type = 'Interface du périphérique'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

Liste ordonnée de formats supportés

VENDOR_ID = 0

VENDOR_ID peut être soit un entier, une liste d’entiers ou un dictionnaire. Si c’est un dictionnaire, cela doit être un dictionnaire de dictionnaires sous la forme:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Un entier ou une liste d’entiers

BCD = None

BCD peut être soit None pour ne pas distingué entre les périphériques basé sur BCD, ou il peut être une liste des nombres BCD de tous les périphériques supportés par ce pilote.

THUMBNAIL_HEIGHT = 68

Hauteur de l’aperçu sur le périphérique

THUMBNAIL_COMPRESSION_QUALITY = 75

Qualité de compression pour les vignettes. Paramétrer ceci proche de 100 pour obtenir une meilleure qualité d’étiquette avec moins d’artefacts de compression. Bien sûr, les vignettes grandiront en conséquence.

WANTS_UPDATED_THUMBNAILS = False

Fixer ceci à True si le périphérique supporte les mises à jours d’aperçus de couverture pendant sync_booklists. Le fixer à True demandera à device.py de rafraîchir les aperçus de couverture pendant la correspondance de livre.

CAN_SET_METADATA = ['title', 'authors', 'collections']

Soit les métadonnées des livres sont fixées via le GUI.

CAN_DO_DEVICE_DB_PLUGBOARD = False

Soit le périphérique peut gérer tableau de métadonnée device_db

path_sep = '/'

Le séparateur de chemin pour les chemins vers les livres sur le périphérique

icon = 'reader.png'

Icône de ce périphérique

UserAnnotation

alias de Annotation

OPEN_FEEDBACK_MESSAGE = None

Le GUI affiche ceci en tant que message dans la barre d’état si ce n’est pas None. Utile si l’ouverture peut prendre du temps

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

Ensemble d’extensions qui sont des « livres virtuels » sur le périphérique et par conséquent ne peuvent être vus/sauvés/ajoutés à la bibliothèque. Par exemple : frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

Message à afficher à l’utilisateur pour les extensions de livre virtuel

NUKE_COMMENTS = None

S’il faut détruire les commentaires dans la copie du livre envoyé au périphérique . Si ce n’est pas None ce devrait être une courte chaîne qui remplacera le commentaire.

MANAGES_DEVICE_PRESENCE = False

Si True, indique que ce pilote gère entièrement la détection de périphérique, l’éjection etc. Si vous réglez à True, vous devez implémenter les méthodes detect_managed_devices et debug_managed_device_detection. Un pilote avec ceci réglé à True est responsable de la détection des périphériques, de la gestion d’une liste d’exclusion des périphérique, une liste de périphérique éjectés, etc. calibre utilisera périodiquement la méthode detect_managed_devices() et si elle retourne un périphérique détecté, calibre appellera open(). open() sera appelé à chaque fois qu’un périphérique est renvoyé même si des appels précédents à open() ont échoués, donc le pilote doit maintenir sa propre liste d’exclusion de périphériques qui ont échoués. Similairement, à l’éjection, calibre utilisera eject() et assume alors que le prochain appel à detect_managed_devices() renvoye None, il appellera post_yank_cleanup().

SLOW_DRIVEINFO = False

Si fixer à True, calibre utilisera la méthode get_driveinfo() après que la liste de livre a été chargée pour obtenire le driveinfo.

ASK_TO_ALLOW_CONNECT = False

Si fixer à True, calibre demandera à l’utilisateur s’il veut gérer le périphérique avec calibre, la première fois qu’il est détecté. Si vous fixez ceci à True, vous devez implémenter get_device_uid() et ignore_connected_device() et get_user_blacklisted_devices() et set_user_blacklisted_devices()

user_feedback_after_callback = None

Fixer ceci à un dictionnaire de la forme {“title”:title, “msg”:msg, “det_msg”:detailed_msg} pour que calibre fasse surgir un message à l’utilisateur après que quelques rappels aient été fait ( actuellement uniquement upload_books). Il faut faire attention à ne pas noyer l’utilisateur par trop de messages. Cette variable est vérifiée après chaque rappel, donc utilisez la uniquement lorsque c’est réellement nécessaire.

classmethod get_open_popup_message()[source]

L’interface graphique affiche ceci comme une popup non modale. Devrait être une instance de OpenPopupMessage

is_usb_connected(devices_on_system, debug=False, only_presence=False)[source]

Renvoye True , device_info si un périphérique géré par cette extension est actuellement connecté.

Paramètres:

devices_on_system – Liste des périphériques actuellement connectés

detect_managed_devices(devices_on_system, force_refresh=False)[source]

Appelé uniquement si MANAGES_DEVICE_PRESENCE est True.

Cherche des périphérique que ce pilote peut gérer. Devrait renvoyer un objet périphérique si un périphérique est trouvé. Cet objet sera passé à la méthode open() en tant que connected_device. Si aucun périphérique n’est trouvé, renvoie None. L’objet renvoyé peut être n’importe quoi, calibre ne l’utilise pas, il est uniquement passé dans open().

Cette méthode est appelée périodiquement par le GUI, donc soyez sûr qu’il n’utilise pas trop de ressource. Utilisez un cache pour pour éviter de scanner répétitivement le système.

Paramètres:

- devices_on_system – Ensemble de périphériques USB trouvés sur le système.

- force_refresh – Si True et que le pilote utilise un cache pour prévenir les scan répétés, le cache doit être flushé.

debug_managed_device_detection(devices_on_system, output)[source]

Appelé uniquement si MANAGES_DEVICE_PRESENCE est True.

Devrait écrire des informations sur les périphériques détectés par le système pour être affiché, qui devrait être un objet ressemblant à un fichier.

Devrait renvoyer True si un périphérique était détecter et effectivement ouvert, sinon False.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Paramètres:

- key – La clef pour débloquer le périphérique

- log_packets – Si True, le paquet envoyé ou reçu au périphérique est mis au registre

- report_progress – Une fonction qui est appelée avec un % de progression (un nombre en 0 et 100) pour différentes tâches. Si on utilise -1 cela veut dire que la tâche n’a aucune information de progression

- detected_device – Les informations de périphérique recueilient par l’analyse de périphérique

can_handle_windows(usbdevice, debug=False)[source]

Une méthode optionnelle pour effectuer de plus amples vérifications sur un périphérique pour savoir si ce pilote est capable de le gérer. Sinon, cela devrait renvoyer False. Cette méthode est appelée uniquement après que le vendeur, l’identifiant du produit et le bcd sont appariés, donc elle peut faire des vérifications nécessitant pas mal de temps. L’implémentation par défaut renvoye True. Cette méthode est appelée uniquement sur Windows. voir aussi can_handle().

Notez que pour les périphériques basés sur USBMS cette méthode par défaut délègue à can_handle(). Donc vous avez seulement besoin d’outrepasser can_handle() dans votre sous-classe de USBMS.

Paramètres:

usbdevice – Un périphérique USB est rapporté par calibre.devices.winusb.scan_usb_devices()

can_handle(device_info, debug=False)[source]

Version Unix de can_handle_windows().

Paramètres:

device_info – Est un tuple de (vid, pid, bcd, manufacturer, product, serial number)

open(connected_device, library_uuid)[source]

Effectue n’importe quel initialisation spécifique de périphérique. Appelé après que le périphérique soit détecté mais avant que n’importe quelle fonction qui communique avec le périphérique. Par exemple: Pour les périphériques qui se présentent comme des périphériques de stockage de masse USB, cette méthode devrait être responsable du montage du périphérique ou si le périphérique a été monté automatiquement, de trouver où il a été monté. La méthode calibre.devices.usbms.device.Device.open() a une implémentation de cette fonction qui devrait servir comme un bon exemple pour les périphériques de stockage de masse USB.

Cette méthode peut créer une exception OpenFeedback pour afficher un message à l’utilisateur.

Paramètres:

- connected_device – Le périphérique que nous essayons actuellement d’ouvrir. C’est un tuple (vendor id, product id, bcd, manufacturer name, product name, device serial number). Toutefois, certains périphériques n’on aucun numéro de séries et sur Windows seul les trois premier champs sont présent, les autres sont None

- library_uuid – L’UUID de la bibliothèque calibre actuelle. Peut être None s’il n’y a pas de bibliohèque (par exemple lorsqu’il est utilisé de l’invité de commande).

eject()[source]

Démonte/éjecte le périphérique de l’OS. Ceci ne vérifie pas s’il y a des travaux GUI en attente qui nécessite communication avec le périphérique.

NOTEZ: Que cette méthode peut ne pas être appelée sur le même fil que le reste des méthodes périphériques.

post_yank_cleanup()[source]

Appelé si l’utilisateur retire le périphériques sans l’avoir éjecter préalablement.

set_progress_reporter(report_progress)[source]

Pose une fonction pour rapporter les information de progression.

Paramètres:

report_progress – Une fonction qui est appelée avec un % de progression (un nombre en 0 et 100) pour différentes tâches. Si on utilise -1 cela veut dire que la tâche n’a aucune information de progression

get_device_information(end_session=True)[source]

Demande ses informations au périphérique. Voir L{DeviceInfoQuery}.

Renvoie:

(nom du périphérique, version du périphérique, version du programme sur le périphérique, type MIME) Le tuple peut parfois avoir un cinquième élément, qui est dictionnaire d’information sur le lecteur. Voir usbms.driver pour un exemple.

get_driveinfo()[source]

Renvoie le dictionnaire driveinfo. Habituellement appélé de get_device_information(), mais si le chargement du driveinfo est lent pour ce lecteur, alors il devrait fixer SLOW_DRIVEINFO. Dans ce cas, cette méthode sera appelée par calibre après que la liste de livre ait été chargée. Notez que ceci n’est pas appelé sur le fil du périphérique, donc le pilote devrait cacher les informations du pilote dans la méthode books() et cette fonction devrait renvoyer les données cachées.

card_prefix(end_session=True)[source]

Renvoie une liste de 2 éléments des préfixes vers les chemins sur les cartes. Si aucune carte n’est présent, None est fixé comme le préfixe de la carte. e.g. (“/place”, “/place2”) (None, “place2”) (“place”, None) (None, None)

total_space(end_session=True)[source]

Récupère l’espace disponible sur les points de montages:

1. Mémoire principale

2. Carte Mémoire A

3. Carte Mémoire B

Renvoie:

Une liste à 3 éléments avec des espaces entier en bit de (1, 2, 3). Si un périphérique particulier n’a pas l’une de ces locations, il devrait retourner à 0.

free_space(end_session=True)[source]

Récupère l’espace disponible sur les points de montage:

1. Mémoire principale

2. Carte A

3. Carte B

Renvoie:

Une liste à 3 éléments avec des espaces entier en bit de (1, 2, 3). Si un périphérique particulier n’a pas l’une de ces locations, il devrait retourner à -1.

books(oncard=None, end_session=True)[source]

Renvoie une liste des livres numérique sur le périphérique.

Paramètres:

oncard – Si “carda” ou “cardb”, renvoie une liste de livres numériques sur la carte de stockage spécifique, sinon renvoie une liste de livres numériques dans la mémoire principale du périphérique. Si une carte est spécifiée et qu’aucun livre n’est dans la carte, renvoie une liste vide.

Renvoie:

Une BookList

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Envoie une liste de livres au périphérique. Si un fichier existe déjà sur le périphérique, il devrait être remplacé. Cette méthode devrait soulever une FreeSpaceError s’il n’y a pas d’espace sur le périphérique. Le texte dans la FreeSpaceError doit contenir le mot « carte » si « on_card » n’est pas None il doit contenir le mot « mémoire ».

Paramètres:

- files – Une liste de chemins

- names – Une liste de noms que les livres devraient avoir une fois envoyé au périphérique. len(names) == len(files)

- metadata – Si n’est pas None, est une liste d’objets Métadonnées. L’idée est d’utiliser la métadonnée pour déterminer où mettre le livre sur le périphérique. len(metadata) == len(files). En plus de la couverture habituelle(chemin vers la couverture), il peut aussi y avoir un attribut d’aperçu, qui devrait être utilisée préférentiellement. L’attribut d’aperçu est de la form (largeur, hauteur, cover_data en jpeg).

Renvoie:

Une liste d’ensemble de tuples à 3 éléments. La liste est faite pour être passée à add_books_to_metadata().

classmethod add_books_to_metadata(locations, metadata, booklists)[source]

Ajoute des emplacements à la liste de livres. cette fonction ne doit pas communiquer avec le périphérique.

Paramètres:

- locations – Résultat d’un appel à L{upload_books}

- metadata – Liste d’objets Métadonénes, même chose que upload_books().

- booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

delete_books(paths, end_session=True)[source]

Supprime les livre aux chemins sur le périphérique.

classmethod remove_books_from_metadata(paths, booklists)[source]

Retire les livres de la liste de métadonnées. Cette fonction ne doit pas communiquer avec le périphérique.

Paramètres:

- paths – les chemins vers les livres sur le périphérique.

- booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

sync_booklists(booklists, end_session=True)[source]

Met à jour les métadonnées sur le périphérique.

Paramètres:

booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

get_file(path, outfile, end_session=True)[source]

Lit le fichier à path sur le périphérique et l’écrit sur un fichier extérieur.

Paramètres:

outfile – un objet ressemblant à un fichier sys.stdout ou le résultat d’un appel à une open()

classmethod config_widget()[source]

Devrait renvoyer un QWdiget. Le QWidget contient les paramètres pour l’interface du périphérique.

classmethod save_settings(settings_widget)[source]

Devrait sauver les paramètres sur le disque. Prend le widget créer dans config_widget() et sauve tous les paramètres sur le disque.

classmethod settings()[source]

Devrait renvoyer un objet opts. Les objets opts devrait au moins avoir un attribut format_map qui est une liste ordonnée de formats pour le périphérique.

set_plugboards(plugboards, pb_func)[source]

fournit au pilote le l’ensemble actuel de tableaux de connexion et une fonction pour sélectionner un tableau de connexion spécifique. Cette méthode est appelée immédiatement avant add_books et sync_booklists.

pb_func est appelable avec la signature suivante:

def pb_func(device_name, format, tableaux de connexion)

Vous donnez le nom actuel du périphérique(soit le nom de classe du périphérique soit DEVICE_PLUGBOARD_NAME), le format qui vous intéresse (un “real” format ou “device_db”), et les tableaux de connexion (ceux donnés par set_plugboards, au même endroit qu’où vous avez eu cette méthode).

Renvoie:

None ou unique tableau de connexion

set_driveinfo_name(location_code, name)[source]

Fixe le nom du périphérique dans le driveinfo à “name”. Ce Paramètre persistera jusqu’à ce que le fichier soit recréer ou que le nom soit changé à nouveau.

Des périphériques n’étant pas des disques devrait implémenter cette méthode basée sur les codes d’emplacement renvoyé par la méthode get_device_information().

prepare_addable_books(paths)[source]

En fonction d’une liste de chemins, renvoie une autre liste de chemins. Ces chemins pointe vers des versions cumulable des livres.

S’il y a une erreur dans la préparation d’un livre, alors au lieu du chemin, la position dans la liste renvoyée pour ce livre devrait être un 3-tuple (original_path, l’exception, retraçage)

startup()[source]

Appelé lorsque calibre démarre le périphérique. Fait toutes les initialisations requises. Notez que de multiples utilisation de la classe peuvent être instanciée, et donc __init__ peut être appelé de nombreuse fois, mais une seule instance utilisera cette méthode. Cette méthode est appelée sur le fil périphérique, pas celui du GUI.

shutdown()[source]

Appelé lorsque calibre s’éteint, soit réellement, soit pour redémarrer. Fais n’importe quel nettoyage nécessaire. Cette méthode est appelée sur le fil périphérique, pas celui du GUI.

get_device_uid()[source]

Doit rendre un identificateur unique pour le périphérique connecté (ceci est appelé immédiatement après un appel réussi à open()). Vous devez implémenter cette méthode si vous fixez ASK_TO_ALLOW_CONNECT = True

ignore_connected_device(uid)[source]

Devrait ignorer le périphérique identifié par l’uid (le résultat d’un appel à get_device_uid()) dans le futur. Vous devez implémenter cette méthode si vous avez fixez ASK_TO_ALLOW_CONNECT = True.. Notez que cette fonction est appelée immédiatement après open(), donc si open() cache un état, le pilote devrait réinitialiser cet état.

get_user_blacklisted_devices()[source]

Renvoie un carte d’uid de périphérique à des noms lisibles pour tous les périphériques que l’utilisateur a demandé d’ignorer.

set_user_blacklisted_devices(devices)[source]

Fixe la liste d’uids de périphérique qui devraient être ignorés par ce pilote.

specialize_global_preferences(device_prefs)[source]

Implémentez cette méthode si votre périphérique veut surpasser une préférence particulière. Vous devez vous assurer que tous les sites d’appel qui veulent la préférence qui peut être outrepassée utilise device_prefs[“quelque_chose”] au lieu de prefs[“quelque_chose”]. Votre méthode devrait appeler device_prefs.set_overrides(pref=val, pref=val, …). Acutellement utilisé pour: gestion de métadonnée (prefs[“manage_device_metadata”])

set_library_info(library_name, library_uuid, field_metadata)[source]

Implémentez cette méthode si vous voulez des informations sur la bibliothèque calibre actuelle. Cette méthode est appelée au démarrage et lorsque la bibliothèque calibre est changée un fois connecté.

is_dynamically_controllable()[source]

Appelée par le gestionnaire de périphérique au lancement des extensions. Si cette méthode renvoie une chaîne, alors a) il supporte le contrôle dynamique d’interface du gestionnaire de périphérique, et b) ce nom doit être utiliser lorsqu’on communique avec l’extension.

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

start_plugin()[source]

Cette méthode est appelée au lancement de l’extension. Cette extension devrait commencer à accepter des connexions de périphériques qu’importe la manière dont ils le font. Si l’extension accepte déjà les connexions, alors ne fait rien.

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

stop_plugin()[source]

Cette méthode est appelée pour arrêter l’extension. Cette extension ne devrait plus accepter de connections et devrait nettoyer derrière lui. Il est probable que cette méthode appel une éteinte. Si cette extension n’accepte déjà plus de connections, alors ne fait rien.

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

get_option(opt_string, default=None)[source]

Renvoie la valeur de l’option indiquée par opt_string. Cette méthode peut être appelée lorsque l’extension n’est pas lancée. Renvoie None si cette option n’existe pas.

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

set_option(opt_string, opt_value)[source]

Fixe la valeur de l’option indiquée par opt_string. Cette méthode peut être appelée lorsque l’extension n’est pas lancée.

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

is_running()[source]

Renvoie True si l’extension est lancée, sinon False

Cette méthode peut être appelée dans le fil GUI. Un pilote qui implémente cette méthode doit être sûr du fil.

synchronize_with_db(db, book_id, book_metadata, first_call)[source]

Appelé durant la correspondance de livre lorsqu’un livre sur un périphérique est appareillé avec une livre de la bdd de calibre. La méthode est responsable pour synchroniser les données du périphérique vers la bdd de calibre (si nécessaire).

La méthode doit renvoyer un tuple à deux valeurs. La première valeur est un ensemble des identifiants de livre calibre changés si la base de donnée calibre a été changée ou None si la base de donnée n’a pas été changée. Si la première valeur est un ensemble vide alors les métadonnée pour le livre sur le périphérique sont mises à jour avec les métadonnées de calibre et renvoyée au périphérique, mais aucun rafraîchissement GUI de ce livre n’est effectué. Ceci est pratique lorsque les données de calibre sont justes mais qu’elles doivent être envoyées au périphérique.

La seconde valeur est elle-même un tuple de 2 valeurs. La première valeur dans le tuple spécifie si un format du livre devrait être envoyé au périphérique. L’idée est de permettre de vérifier que le livre sur le périphérique est le même que le livre dans calibre. Cette valeur doit être None si aucun livre ne doit être envoyé, sinon renvoie le nom du fichier de base sur le périphérique (une chaîne comme foobar.epub). Soyez sûr d’inclure l’extension dans le nom. Le sous système du périphérique construira une tâche send_books pour tous les livres qui n’ont pas une valeur None . Note: à part pour récupérer l’extension par après, le nom est ignoré dans le cas où le périphérique utilise un schéma pour générer le nom de fichier, ce que la plupart font. La seconde valeur dans le tuple renvoyé indique si le format est prévu. Renvoie True s’il l’est, sinon renvoie False. calibre affichera une boîte de dialogue à l’utilisateur pour afficher tous les livres prévus.

Extrêmement important: cette méthode est appelée sur le fil GUI. Elle doit être en accord avec le fil du gestionnaire de périphérique.

book_id: l’identificateur calibre pour le livre dans la base de données. book_metadata: la métadonnée pour le livre venant du périphérique. first_call: True si c’est le premier appel durant une synchronisation, sinon False.

class calibre.devices.interface.BookList(oncard, prefix, settings)[source]

Bases : list

Un liste de livres. Chaque livre doit avoir les champs

1. titre

2. auteurs

3. taille (taille du fichier du livre)

4. date (un tuple temps UTC)

5. chemin (chemin vers le livre sur le périphérique)

6. aperçu (peut être None) l’aperçu est soit un str/bytes avec les données de l’image ou il devrait avoir un attribut image_path qui contient un chemin absolu (dépendant de la plateforme) vers le livre

7. identifiant (une liste de chaîne, peut être vide).

supports_collections()[source]

Renvoie True si le périphérique supporte les collections de livre pour cette liste de livre.

add_book(book, replace_metadata)[source]

Ajoute le livre dans la liste de livre. L’idée est de maintenir les métadonnées interne du périphérique. Renvoie True si la liste de livre doit être synchronisée.

remove_book(book)[source]

Supprime un livre de la liste de livre. Corrige toutes les métadonnées sur le périphérique en même temps

get_collections(collection_attributes)[source]

Renvoie un dictionnaire de collections créée avec collection_attributes. Chaque entrée dans le dictionnaire est sous la forme nom de collection:[liste de livres]

La liste de livre est triée par titre, sauf pour les collections créée à partir de séries, auquel cas series_index est utilisé.

Paramètres:

collection_attributes – Un liste d’attribut du livre

Périphérique basé sur du stockage de masse USB

La classe de base pour ces périphériques est calibre.devices.usbms.driver.USBMS. Cette classe hérite certaines de ces fonctionnalité de ces bases, documentée plus bas. Un pilote basique typique USBMS (USB Mass Storage) ressemble à ceci :

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)[source]

Bases : DeviceConfig, DevicePlugin

Cette classe fournit une logique commune à tous les pilotes pour les périphériques qui s’exportent comme des périphériques de stockage de masse USB. Fournit des implémentation pour monter/éjecter un périphérique USBMS sur toute les plateformes.

VENDOR_ID = 0

VENDOR_ID peut être soit un entier, une liste d’entiers ou un dictionnaire. Si c’est un dictionnaire, cela doit être un dictionnaire de dictionnaires sous la forme:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Un entier ou une liste d’entiers

BCD = None

BCD peut être soit None pour ne pas distingué entre les périphériques basé sur BCD, ou il peut être une liste des nombres BCD de tous les périphériques supportés par ce pilote.

WINDOWS_MAIN_MEM = None

Une chaîne identifiant la mémoire principale du périphérique dans les chaines d’identifications PnP de Windows. Ceci peut être None, une chaîne, une liste de chaîne ou une expression régulière compilée

WINDOWS_CARD_A_MEM = None

Une chaîne identifiant la première carte du périphérique dans les chaines d’identifications PnP de Windows. Ceci peut être None, une chaîne, une liste de chaîne ou une expression régulière compilée

WINDOWS_CARD_B_MEM = None

Une chaîne identifiant la deuxième carte du périphérique dans les chaines d’identifications PnP de Windows. Ceci peut être None, une chaîne, une liste de chaîne ou une expression régulière compilée

OSX_MAIN_MEM_VOL_PAT = None

Utilisé par la détection du nouveau pilote pour dissocier la mémoire principale des cartes mémoires. Devrait-être une expression régulière qui correspond au point de montage de la mémoire principale assignée par macOS

BACKLOADING_ERROR_MESSAGE = None

MAX_PATH_LEN = 250

La longueur maximum des chemins créés sur le périphérique

NEWS_IN_FOLDER = True

Place les nouvelles dans son propre fichier

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Paramètres:

- key – La clef pour débloquer le périphérique

- log_packets – Si True, le paquet envoyé ou reçu au périphérique est mis au registre

- report_progress – Une fonction qui est appelée avec un % de progression (un nombre en 0 et 100) pour différentes tâches. Si on utilise -1 cela veut dire que la tâche n’a aucune information de progression

- detected_device – Les informations de périphérique recueilient par l’analyse de périphérique

set_progress_reporter(report_progress)[source]

Pose une fonction pour rapporter les information de progression.

Paramètres:

report_progress – Une fonction qui est appelée avec un % de progression (un nombre en 0 et 100) pour différentes tâches. Si on utilise -1 cela veut dire que la tâche n’a aucune information de progression

card_prefix(end_session=True)[source]

Renvoie une liste de 2 éléments des préfixes vers les chemins sur les cartes. Si aucune carte n’est présent, None est fixé comme le préfixe de la carte. e.g. (“/place”, “/place2”) (None, “place2”) (“place”, None) (None, None)

total_space(end_session=True)[source]

Récupère l’espace disponible sur les points de montages:

1. Mémoire principale

2. Carte Mémoire A

3. Carte Mémoire B

Renvoie:

Une liste à 3 éléments avec des espaces entier en bit de (1, 2, 3). Si un périphérique particulier n’a pas l’une de ces locations, il devrait retourner à 0.

free_space(end_session=True)[source]

Récupère l’espace disponible sur les points de montage:

1. Mémoire principale

2. Carte A

3. Carte B

Renvoie:

Une liste à 3 éléments avec des espaces entier en bit de (1, 2, 3). Si un périphérique particulier n’a pas l’une de ces locations, il devrait retourner à -1.

windows_sort_drives(drives)[source]

Appelée pour différencier la mémoire principale et les cartes mémoires pour les périphériques qui ne peuvent les différencier sur base de WINDOWS_CARD_NAME. par ex. : L’EB600

can_handle_windows(usbdevice, debug=False)[source]

Une méthode optionnelle pour effectuer de plus amples vérifications sur un périphérique pour savoir si ce pilote est capable de le gérer. Sinon, cela devrait renvoyer False. Cette méthode est appelée uniquement après que le vendeur, l’identifiant du produit et le bcd sont appariés, donc elle peut faire des vérifications nécessitant pas mal de temps. L’implémentation par défaut renvoye True. Cette méthode est appelée uniquement sur Windows. voir aussi can_handle().

Notez que pour les périphériques basés sur USBMS cette méthode par défaut délègue à can_handle(). Donc vous avez seulement besoin d’outrepasser can_handle() dans votre sous-classe de USBMS.

Paramètres:

usbdevice – Un périphérique USB est rapporté par calibre.devices.winusb.scan_usb_devices()

open(connected_device, library_uuid)[source]

Effectue n’importe quel initialisation spécifique de périphérique. Appelé après que le périphérique soit détecté mais avant que n’importe quelle fonction qui communique avec le périphérique. Par exemple: Pour les périphériques qui se présentent comme des périphériques de stockage de masse USB, cette méthode devrait être responsable du montage du périphérique ou si le périphérique a été monté automatiquement, de trouver où il a été monté. La méthode calibre.devices.usbms.device.Device.open() a une implémentation de cette fonction qui devrait servir comme un bon exemple pour les périphériques de stockage de masse USB.

Cette méthode peut créer une exception OpenFeedback pour afficher un message à l’utilisateur.

Paramètres:

- connected_device – Le périphérique que nous essayons actuellement d’ouvrir. C’est un tuple (vendor id, product id, bcd, manufacturer name, product name, device serial number). Toutefois, certains périphériques n’on aucun numéro de séries et sur Windows seul les trois premier champs sont présent, les autres sont None

- library_uuid – L’UUID de la bibliothèque calibre actuelle. Peut être None s’il n’y a pas de bibliohèque (par exemple lorsqu’il est utilisé de l’invité de commande).

eject()[source]

Démonte/éjecte le périphérique de l’OS. Ceci ne vérifie pas s’il y a des travaux GUI en attente qui nécessite communication avec le périphérique.

NOTEZ: Que cette méthode peut ne pas être appelée sur le même fil que le reste des méthodes périphériques.

post_yank_cleanup()[source]

Appelé si l’utilisateur retire le périphériques sans l’avoir éjecter préalablement.

sanitize_callback(path)[source]

Rappel pour permettre aux pilotes individuels de périphérique d’outrepasser l’expurgation des chemins utilisé par create_upload_path().

filename_callback(default, mi)[source]

Rappel pour permettre aux pilotes de changer le nom de fichier par défaut fixé par create_upload_path().

sanitize_path_components(components)[source]

Effectue n’importe quel expurgation de chemins spécifique au périphérique pour les fichiers devant être envoyés au périphérique

get_annotations(path_map)[source]

Résous path_map pour annotation_map pour les fichier trouvés sur le périphérique

add_annotation_to_library(db, db_id, annotation)[source]

Ajoute une annotation dans la bibliothèque calibre

class calibre.devices.usbms.cli.CLI[source]

class calibre.devices.usbms.driver.USBMS(plugin_path)[source]

Bases : CLI, Device

Les classes de base pour tous les périphériques USBMS. Implémente la logique d’envoi/réception/màj/métadonnée/encachement de métadonnée/etc.

description = 'Communiquer avec une liseuse de livre numérique.'

Une courte chaine décrivant ce que fait l’extension

author = 'John Schember'

L’auteur de cette extension

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

booklist_class

alias de BookList

book_class

alias de Book

FORMATS = []

Liste ordonnée de formats supportés

CAN_SET_METADATA = []

Soit les métadonnées des livres sont fixées via le GUI.

get_device_information(end_session=True)[source]

Demande ses informations au périphérique. Voir L{DeviceInfoQuery}.

Renvoie:

(nom du périphérique, version du périphérique, version du programme sur le périphérique, type MIME) Le tuple peut parfois avoir un cinquième élément, qui est dictionnaire d’information sur le lecteur. Voir usbms.driver pour un exemple.

set_driveinfo_name(location_code, name)[source]

Fixe le nom du périphérique dans le driveinfo à “name”. Ce Paramètre persistera jusqu’à ce que le fichier soit recréer ou que le nom soit changé à nouveau.

Des périphériques n’étant pas des disques devrait implémenter cette méthode basée sur les codes d’emplacement renvoyé par la méthode get_device_information().

books(oncard=None, end_session=True)[source]

Renvoie une liste des livres numérique sur le périphérique.

Paramètres:

oncard – Si “carda” ou “cardb”, renvoie une liste de livres numériques sur la carte de stockage spécifique, sinon renvoie une liste de livres numériques dans la mémoire principale du périphérique. Si une carte est spécifiée et qu’aucun livre n’est dans la carte, renvoie une liste vide.

Renvoie:

Une BookList

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Envoie une liste de livres au périphérique. Si un fichier existe déjà sur le périphérique, il devrait être remplacé. Cette méthode devrait soulever une FreeSpaceError s’il n’y a pas d’espace sur le périphérique. Le texte dans la FreeSpaceError doit contenir le mot « carte » si « on_card » n’est pas None il doit contenir le mot « mémoire ».

Paramètres:

- files – Une liste de chemins

- names – Une liste de noms que les livres devraient avoir une fois envoyé au périphérique. len(names) == len(files)

- metadata – Si n’est pas None, est une liste d’objets Métadonnées. L’idée est d’utiliser la métadonnée pour déterminer où mettre le livre sur le périphérique. len(metadata) == len(files). En plus de la couverture habituelle(chemin vers la couverture), il peut aussi y avoir un attribut d’aperçu, qui devrait être utilisée préférentiellement. L’attribut d’aperçu est de la form (largeur, hauteur, cover_data en jpeg).

Renvoie:

Une liste d’ensemble de tuples à 3 éléments. La liste est faite pour être passée à add_books_to_metadata().

upload_cover(path, filename, metadata, filepath)[source]

Envoie les couvertures de livre au périphérique. L’implémentation par défaut ne fait rien.

Paramètres:

- path – Le chemin complet vers le dossier où se trouve le livre associé.

- filename – Le nom du fichier du livre sans l’extension.

- metadata – métadonnée appartenant au livre. Utilisez metadata.thumbnail pour les couvertures

- filepath – Le chemin absolu vers le livre numérique

add_books_to_metadata(locations, metadata, booklists)[source]

Ajoute des emplacements à la liste de livres. cette fonction ne doit pas communiquer avec le périphérique.

Paramètres:

- locations – Résultat d’un appel à L{upload_books}

- metadata – Liste d’objets Métadonénes, même chose que upload_books().

- booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

delete_books(paths, end_session=True)[source]

Supprime les livre aux chemins sur le périphérique.

remove_books_from_metadata(paths, booklists)[source]

Retire les livres de la liste de métadonnées. Cette fonction ne doit pas communiquer avec le périphérique.

Paramètres:

- paths – les chemins vers les livres sur le périphérique.

- booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

sync_booklists(booklists, end_session=True)[source]

Met à jour les métadonnées sur le périphérique.

Paramètres:

booklists – En tuple contenant le résultat d’appels à (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=”cardb”)`).

classmethod normalize_path(path)[source]

Renvoie le chemin avec les séparateurs de chemin natif à la platforme

Actions interface utilisateur

Si vous ajoutez votre propre extension dans un fichier ZIP, vous devriez sous-classer InterfaceActionBase et InterfaceAction. La méthode load_actual_plugin() de votre sous-classe InterfaceActionBase doit renvoyer un objet instancié de votre sous-classe InterfaceBase.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[source]

Bases : QObject

Une extension représentant une « action » qui peut être effectuée dans le GUI. Tous les menus de la barre d’outil et les menus contextuels sont implémentés par ces extensions.

Notez que cette classe est la classe de base pour ces extensions, toutefois, pour intégrer l’extension avec le système d’extension de calibre, vous devez avoir une classe englobante qui référence la vrai extension.Voir le module calibre.customize.builtins pour des exemples.

Si deux objets InterfaceAction ont le même nom, celui avec la priorité la plus haute à préséance.

Les sous-classes devrait implémenter les méthodes library_changed(), location_selected(), shutting_down(), initialization_complete() et tag_browser_context_action().

Une fois initialisé, cette extension à accès au Gui principal de calibre via le membre gui. Vous pouvez accéder d’autres extensions par nom, par exemple:

self.gui.iactions['Save To Disk']

Pour accéder à la réelle extension, utiliser l’attribut interface_action_base_plugin, cet attribut devient disponible uniquement après que l’extension ait été initialisée. Utile si vous souhaitez utiliser des méthodes de la classe extension comme do_user_config().

La QAction spécifiée par action_spec est automatiquement créée et rendue disponible sous self.qaction.

name = 'Implement me'

Le nom de l’extension. Si deux extensions avec le même nom sont présentes, celle avec la plus haute priorité à préséance.

priority = 1

La priorité de l’extension. Si deux extensions avec le même nom sont présentes, celle avec la plus haute priorité a préséance.

popup_type = 1

Le type de menu contextuel pour lorsque cette extension est ajoutée à la barre d’outils

auto_repeat = False

Si cette action devrait être répétée automatiquement lorsque son raccourci est maintenant enfoncé.

action_spec = ('text', 'icon', None, None)

De la forme : (texte, icon_path, infobulle,raccourcis clavier) icône, infobulle et raccourcis clavier peuvent être None. Le raccourcis doit être une chaîne, None ou un tuple de raccourcis. Si None, aucune touche n’est enregistrée pour correspondre à l’action. Si vous entrez un tuple vide, alors le raccourcis est enregistré sans attache spécifique à une touche.

action_shortcut_name = None

Si ce n’est pas None, utilisé pour le nom affiché de l’utilisateur lors de la personnalisation des raccourcis clavier pour la spécification d’action ci-dessus au lieu de action_spec[0]

action_add_menu = False

Si True, un menu est automatiquement créé et ajouté à self.qaction

action_menu_clone_qaction = False

Si True, un clône de self.qaction est ajouté au menu de self.qaction. Si vous souhaitez que le texte de cette action soit différent de self.qaction, fixer cette variable aux nouveau texte

dont_add_to = frozenset({})

Ensemble d’emplacements vers lesquels cette action ne doit pas être ajoutée. Voir all_locations pour une liste des emplacements possible

dont_remove_from = frozenset({})

Ensemble d’emplacements d’où cette action ne doit pas être supprimé. Voir all_locations pour une liste des emplacements possible

action_type = 'global'

Type d’action. “current” signifie qu’il agit sur la vue courante, “global” représente une action qui ne change pas la vue courante, mais plutôt calibre tout entier

accepts_drops = False

Si True, alors cette InterfaceAction aura l’opportunité d’interagir avec les événements glisser-déposer. Voir les méthodes accept_enter_event(), :meth`:accept_drag_move_event`, drop_event() pour plus de détails.

accept_enter_event(event, mime_data)[source]

Cette méthode devrait renvoyer True ssi cette action d’interface est capable de gérer les événements glisser. Ne pas appeler accepter/ignorer sur l’événement, ceci sera géré par l’UI de calibre.

accept_drag_move_event(event, mime_data)[source]

Cette méthode devrait renvoyer True ssi cette action d’interface est capable de gérer les événements glisser. Ne pas appeler accepter/ignorer sur l’événement, ceci sera géré par l’UI de calibre.

drop_event(event, mime_data)[source]

Cette méthode devrait effectuer des actions utiles et renvoyer True ssi cette action interface est capable de gérer les événement déposer. Ne pas appeler accepter/ignorer sur cet événement, cela sera géré par l’UI de calibre. Vous ne devriez pas effectuer d’opérations longues/bloquantes avec cette fonction. À la place, émettez un signal ou utiliser un QTimer.singleShot et renvoyez rapidement. Voir les actions intégrées pour des exemples.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[source]

Une méthode commode pour ajouter facilement des actions à un QMenu. Renvoie la QAction créée. Cette action a un attribut de plus, calibre_shortcut_unique_name qui s’il n’est pas None réfère à un nom unique sous lequel cette action est enregistrée dans le gestionnaire du clavier.

Paramètres:

- menu – Le QMenu auquel la nouvelle action créée sera ajouté

- unique_name – Un nom unique pour cette action, ceci doit être globalement unique, donc faites en sorte qu’il soit le plus descriptif possible. Dans le doute, ajoutez-y un UUID

- text – Le texte de l’action.

- icon – Soit une QIcon ou un nom de fichier. Le nom de fichier est passé aux QIcon.ic() intégré, donc vous ne devez pas passer le chemin absolu vers le dossier d’images.

- shortcut – Une chaîne, un ensemble de chaînes, None ou False. Si False, aucun raccourci clavier ne sera enregistré pour cette action. Si None, un raccourci clavier sans touche par défaut est enregistré. La chaîne et la liste de chaîne enregistre un raccourci avec une touche par défaut comme spécifié.

- description – Une description pour cette action. Utilisée pour fixer les infobulles.

- triggered – Un appelable qui est connecté au signal déclenché de l’action créée.

- shortcut_name – Le texte affiché à l’utilisateur lors de la personnalisation des raccourcis clavier pour cette action. Par défaut il est fixer à une valeur text.

- persist_shortcut – Les raccourcis pour les actions qui n’apparaissent pas toujours, ou qui dépendent de la bibliothèque, peuvent disparaître lorsque d’autres raccourcis clavier sont édités, sauf si « Persist_shortcut » est réglé sur True.

load_resources(names)[source]

Si cette extension est donnée dans un fichier ZIP (extension ajoutée par l’utilisateur), cette méthode vous permettra de charger des ressources du fichier ZIP.

Par exemple pour charger une image

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

Paramètres:

names – Liste de chemins vers les ressources dans le ZIP utilisant / comme séparateur

Renvoie:

Un dictionnaire sous la forme {name : file_contents}. Tout nom qui n’est pas trouvé dans le fichier ZIP ne sera pas présent dans le dictionnaire.

genesis()[source]

Paramètre cette extension. Uniquement appelé une fois lors de l’initialisation. self.gui est disponible. L’action spécifiée par action_spec est disponible à self.qaction.

location_selected(loc)[source]

Appelé dès que la liste de livre étant affichée dans calibre change. Actuellement, les valeur pour loc sont: library, main, card and cardb.

Cette méthode devrait activer/désactiver cette action et ces sous-actions en fonction de l’emplacement

library_about_to_change(olddb, db)[source]

Appelé dès que la bibliothèque actuelle est changée.

Paramètres:

- olddb – La LibraryDatabase correspondant à la bibliothèque précédente

- db – La LibraryDatabase correspondant à la nouvelle bibliothèque.

library_changed(db)[source]

Appelé dès que la bibliothèque actuelle est changée.

Paramètres:

db – La LibraryDatabase correspondant à la bibliothèque actuelle

gui_layout_complete()[source]

Appelée une fois par action lorsque l’affichage du Gui principal est complété. Si votre action nécessite de faire des changements dans l’affichage, elles devraient être faite ici, plutôt que dans initialization_complete().

initialization_complete()[source]

Appelée un fois par action lorsque l’initiation du GUI principal est complète.

tag_browser_context_action(index)[source]

Appelé lors de l’affichage du menu contextuel dans le Navigateur d’étiquettes. index est le QModelIndex qui pointe vers l’élément du Navigateur d’étiquettes qui a été cliqué avec le bouton droit. Testez sa validité avec index.valid() et obtenez l’objet TagTreeItem sous-jacent avec index.data(Qt.ItemDataRole.UserRole). Tout objet d’action produit par cette méthode sera ajouté au menu contextuel.

shutting_down()[source]

Appelée une fois par extension lorsque le GUI principal est en train de s’éteindre. Relâche toute les ressources utilisée, mais essaye de ne pas bloquer l’arrêt pour un longue période.

class calibre.customize.InterfaceActionBase(*args, **kwargs)[source]

Bases : Plugin

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

L’auteur de cette extension

type = 'Action interface utilisateur'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

can_be_disabled = False

Si False, l’utilisateur ne pourra pas désactiver cette extension. À utiliser avec soin.

load_actual_plugin(gui)[source]

Cette méthode doit renvoyer l’extension d’action interface correspondante.

Extensions préférences
class calibre.customize.PreferencesPlugin(plugin_path)[source]

Bases : Plugin

Une extension représentant un widget affiché dans la boîte de dialogue des préférences.

Cette extension n’a qu’un méthode importante create_widget(). Les différents champs de l’extension contrôlent comment c’est catégorisé dans l’UI.

supported_platforms = ['windows', 'osx', 'linux']

Liste des plateformes où fonctionne cette extension. Par exemple ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

L’auteur de cette extension

type = 'Préférences'

Le type de cette extension. Utilisé pour catégorisé les extensions dans le GUI

can_be_disabled = False

Si False, l’utilisateur ne pourra pas désactiver cette extension. À utiliser avec soin.

config_widget = None

Importe le chemin vers le module qui contient une classe nommée ConfigWidget qui implémente ConfigWidgetInterface. Utilisé par create_widget().

category_order = 100

Où dans la liste de catégories la catégorie de cette extension devrait se trouver.

name_order = 100

Où dans la liste de noms d’une catégorie, le gui_name de cette extension devrait se trouver

category = None

La catégorie où devrait se trouver cette extension

gui_category = None

Le nom de catégorie affiché à l’utilisateur pour cette extension

gui_name = None

Le nom affiché à l’utilisateur pour cette extension

icon = None

L’icône pour cette extension, devrait être un chemin absolu

description = None

La description pour les infobulles etc

create_widget(parent=None)[source]

Crée et renvoie le Qt widget effectivement utilisé pour paramétrer ce groupe de préférence. Le widget doit implémenter la calibre.gui2.preferences.ConfigWidgetInterface.

L’implémentation par défaut utilise config_widget pour instancié le widget.

class calibre.gui2.preferences.ConfigWidgetInterface[source]

Cette classe définit l’interface que tous les widgets affichée dans la boîte de dialogues de Préférences doit implémenter. Voir ConfigWidgetBase pour une classe de base qui implémente cette interface et définit aussi des méthodes pratiques.

changed_signal = None

Ce signal doit être émis dès que l’utilisateur change une valeur de ce widget

supports_restoring_to_defaults = True

Fixer à TRue ssi la méthode ConfigWidgetBase est implémentée.

restore_defaults_desc = 'Restaurer les réglages aux valeurs par défaut. Vous devez cliquer sur Appliquer pour enregistrer réellement les réglages par défaut.'

L’infobulle pour le bouton « Restauration des paramètres par défaut »

restart_critical = False

Si True, le la boite de dialogue de Préférence ne permettra pas à l’utilisateur d’ajouter d’autres préférences. A de l’effet uniquement si commit() renvoie True.

genesis(gui)[source]

Appelé une fois avant que le widget ne soit affiché, devrait effectuer les paramétrages nécessaire

Paramètres:

gui – L’interface graphique utilisateur principale de calibre

initialize()[source]

Devrait définir toutes les valeurs de configuration à leurs valeurs initiales (les valeurs stockées dans les fichiers de configuration). L’instruction « return » est facultative. Renvoie False si la boîte de dialogue ne doit pas être affichée.

restore_defaults()[source]

Devrait fixer toutes les valeurs de configuration à leur valeur par défaut.

commit()[source]

Sauve tout changement de paramètre. Renvoie True si les changements requière un redémarrage, sinon False. Crée un exception :class:`AbortCommit pour indiquer qu’une erreur est survenue. Vous êtes responsable de donner des informations à l’utilisateur sur ce qu’est l’erreur et comment la corriger.

refresh_gui(gui)[source]

Appelée une fois que ce widget est engagé. Responsable la relecture par le GUI de tous les paramètres modifiés. Notez que par défaut le GUI réinitialise de toutes façon différents éléments, donc la plupart des widgets ne nécessitent pas cette méhode.

initial_tab_changed()[source]

Appelé si l’onglet initialement affiché est modifié avant l’affichage du widget, mais après son initialisation.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[source]

Une classe de base qui contient du code pour ajouter facilement des widgets de configuration standards comme des cases, des listes déroulantes, des zones de textes, etc. Voir la méthode register().

Cette classe gère automatiquement changement de notification, les remises aux paramètres par défaut, la traduction entre les objets GUI et les objets configuration, etc. pour l’enregistrement de paramètres.

Si votre widget de configuration hérite de cette classe mais inclus des paramètres qui ne sont pas enregistrés, vous devriez outrepasser les méthodes ConfigWidgetInterface et appelé les méthodes de base à l’intérieur des dérogations.

changed_signal

Ce signal doit être émis dès que l’utilisateur change une valeur de ce widget

supports_restoring_to_defaults = True

Fixer à TRue ssi la méthode ConfigWidgetBase est implémentée.

restart_critical = False

Si True, le la boite de dialogue de Préférence ne permettra pas à l’utilisateur d’ajouter d’autres préférences. A de l’effet uniquement si commit() renvoie True.

register(name, config_obj, gui_name=None, choices=None, restart_required=False, empty_string_is_None=True, setting=<class 'calibre.gui2.preferences.Setting'>)[source]

Enregistrer un paramètre.

Paramètres:

- name – Le nom du paramètre

- config_obj – Un configuration qui lit/écrit le paramètre

- gui_name – Le nom de l’objet GUI qui présente une interface à changer par le paramètre. Par défaut, il est présumé être 'opt_' + name.

- choices – Si le paramètre est un paramètre de base à plusieurs choix (une liste déroulante), la liste de choix. La liste est une liste de tuples sous la forme [(nom GUI, valeur), ...]

- setting – La classe responsable de la gestion de ce paramètre. La classe par défaut gère presque ous les cas, donc ce paramètre est rarement utilisé.

initialize()[source]

Devrait définir toutes les valeurs de configuration à leurs valeurs initiales (les valeurs stockées dans les fichiers de configuration). L’instruction « return » est facultative. Renvoie False si la boîte de dialogue ne doit pas être affichée.

commit(*args)[source]

Sauve tout changement de paramètre. Renvoie True si les changements requière un redémarrage, sinon False. Crée un exception :class:`AbortCommit pour indiquer qu’une erreur est survenue. Vous êtes responsable de donner des informations à l’utilisateur sur ce qu’est l’erreur et comment la corriger.

restore_defaults(*args)[source]

Devrait fixer toutes les valeurs de configuration à leur valeur par défaut.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande

Interface en ligne de commande

Note

Sous macOS, les outils en de ligne de commande sont à l’intérieur de la suite logicielle calibre, par exemple, si vous installez calibre dans /Applications les outils de ligne de commande sont dans /Applications/calibre.app/Contents/MacOS/. Donc, par exemple /Applications/calibre.app/Contents/MacOS/ebook-convert.

Commandes documentées

- 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

Commandes non documentées

- ebook-device

- markdown-calibre

Vous pouvez voir l’utilisation des commandes non documentées en les exécutants sans arguments dans un terminal

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibre

calibre

calibre [options] [path_to_ebook ou url calibre ...]

Lancez l’interface graphique principale de calibre et ajoutez éventuellement le livre numérique à path_to_ebook vers la base de données. Vous pouvez également spécifier des URL calibre pour effectuer différentes actions, tel que le simple ajout de livres. Par exemple :

calibre://view-book/test_library/1842/epub
ouvrira le livre avec le numéro d’identification 1842 au format EPUB de la bibliothèque
« test_library » dans la visionneuse de livre numérique calibre. Les noms des bibliothèques sont les noms de dossiers des bibliothèques dont les espaces sont remplacés par des caractères de soulignement. Une description complète des diverses actions basées sur l’URL se trouve dans le Manuel de l’utilisateur.

Chaque fois que vous passez à calibre des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--detach

Détacher du terminal de contrôle, s'il y en a un (linux uniquement)

--help, -h

affiche ce message d'aide et quitte

--ignore-plugins

Ignorer les extensions personnalisées, utile si vous installez une extension qui empêche calibre de démarrer.

--no-update-check

Ne pas vérifier les mises à jour

--shutdown-running-calibre, -s

Provoque la fermeture de la session de calibre en cours (si elle existe). Remarquez que s’il y a des tâches en cours, elles seront annulées sur le champ. À utiliser avec prudence.

--start-in-tray

Démarrer en mode minimisé dans la zone de notification.

--verbose, -v

Ignoré, ne pas utiliser. Présent uniquement pour d'anciennes raisons

--version

affiche le numéro de version du logiciel et quitte

--with-library

Utiliser la bibliothèque présente dans le dossier spécifié.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibre-customize

calibre-customize

options calibre-customize

Personnaliser calibre en chargeant des extensions externes.

Chaque fois que vous passez à calibre-customize des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--add-plugin, -a

Ajouter une extension en précisant le chemin de l'archive ZIP qui la contient.

--build-plugin, -b

Pour les développeurs de plugin: chemin vers le dossier où vous développez le plugin. Cette commande va automatiquement compresser le plugin et le mettre à jour dans calibre.

--customize-plugin

Personnaliser le plugin. Indiquez le nom du plugin et la chaîne de personnalisation, séparés par une virgule. La chaîne de personnalisation est la même que celle que vous saisissez lorsque vous personnalisez le plugin dans l'interface graphique principale de Calibre.

--disable-plugin

Désactiver l’extension nommée

--enable-plugin

Activer l’extension nommée

--help, -h

affiche ce message d'aide et quitte

--list-plugins, -l

Lister toutes les extensions installées

--remove-plugin, -r

Supprimer une extension personnalisée par son nom. N’a pas d’effet sur les extensions intégrées

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibre-debug

calibre-debug

calibre-debug [options]

Diverses interfaces de ligne de commande utiles pour le débogage de calibre. Sans option, cette commande lance un interpréteur Python embarqué. Vous pouvez également exécuter l’interface principale calibre, la visionneuse de livres numériques calibre et l’éditeur calibre en mode débogage.

Il contient également des interfaces pour divers éléments de calibre qui ne disposent pas d’outils dédiés en ligne de commande, comme le sous-ensemble de polices, l’outil de comparaison de livres numériques, etc.

Vous pouvez également utiliser calibre-debug pour exécuter des scripts autonomes. Pour ce faire, utilisez-le comme suit :

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

Tout ce qui suit le -- est transmis au script. Vous pouvez aussi utiliser calibre-debug comme shebang dans les scripts, comme ceci :

#!/usr/bin/env -S calibre-debug -e -- –

Chaque fois que vous passez à calibre-debug des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--add-simple-plugin

Ajouter une extension simple (par ex. une extension qui consiste en un unique fichier .py), en spécifiant le chemin vers le fichier py contenant le code de l'extension.

--command, -c

Exécuter du code Python.

--debug-device-driver, -d

Déboguer la détection du périphérique

--default-programs

(Dés)enregistrer calibre des Programmes par Défaut de Windows --default-programs = (register|unregister)

--diff

Exécuter l'outil calibre diff. Par exemple: calibre-debug --diff fichier1 fichier2

--edit-book

Lancez l’Éditeur de livre calibre en mode débogage.

--exec-file, -e

Exécuter le code Python dans le fichier

--explode-book, -x

Décompacte le livre dans le dossier spécifié. Utilisation : -x file.epub output_dir Exporte le livre comme une collection de fichiers HTML et de métadonnées, qui peuvent être édités en utilisant des outils standards d'édition HTML. Fonctionne avec des fichiers EPUB, AZW3, HTMLZ et DOCX

--export-all-calibre-data

Exporter toutes les données calibre (livres/réglages/plugins). Normalement, on vous demandera le dossier d'exportation et les bibliothèques à exporter. Vous pouvez également les spécifier en tant qu'arguments de ligne de commande pour sauter les questions. Utilisez des chemins absolus pour le dossier d'exportation et les bibliothèques. Le mot-clé spécial "all" peut être utilisé pour exporter toutes les bibliothèques. Exemples : calibre-debug --export-all-calibre-data # pour l'utilisation interactive calibre-debug --export-all-calibre-data /path/to/empty/export/folder /path/to/library/folder1 /path/to/library2 calibre-debug --export-all-calibre-data /export/folder all # exporter toutes les bibliothèques connues

--fix-multiprocessing

Pour usage interne

--gui, -g

Exécuter le GUI avec le débogage activé. La sortie de débogage est inscrite dans stdout et stderr.

--gui-debug

Exécuter le GUI avec une console de débogage, inscrite à l'emplacement spécifié. Pour usage interne uniquement, utiliser l'option -g pour exécuter le GUI en mode débogage.

--help, -h

affiche ce message d'aide et quitte

--implode-book, -i

Re-compacter un livre précédemment décompacté. Utilisation : -i output_dir file.epub Importe le livre depuis les fichiers dans output_dir qui avaient été créé par une un appel précédent à call to --explode-book. Assurez-vous de spécifier le même type de fichier que celui utilisé lors du décompactage.

--import-calibre-data

Importer les données calibre précédemment exportées

--inspect-mobi, -m

Inspecter le(s) fichier(s) MOBI à/aux (l')emplacement(s) spécifié(s)

--paths

Sort les emplacements nécessaires pour configurer l'environnement de calibre

--run-plugin, -r

Exécuter une extension qui fournit une interface en ligne de commande. Par exemple : calibre-debug -r "Plugin name" -- file1 --option1 Tout ce qui se trouve après le -- sera passé à l'extension en tant qu'arguments.

--run-test, -t

Exécuter le(s) test(s) nommé(s). Utilisez la valeur spéciale "all" pour exécuter tous les tests. Si le nom du test commence par un point, il est supposé être un nom de module. Si le nom du test commence par @, il s'agit d'un nom de catégorie.

--run-without-debug

Ne pas exécuter avec l'option DEBUG activée

--shutdown-running-calibre, -s

Provoque la fermeture de la session de calibre en cours (si elle existe). Remarquez que s’il y a des tâches en cours, elles seront annulées sur le champ. À utiliser avec prudence.

--subset-font, -f

Créer un sous-ensemble avec les polices spécifiées. Utilisez -- après cette option pour passer l'option au programme de sous-ensembles de police.

--test-build

Tester les modules binaires en construction

--version

affiche le numéro de version du logiciel et quitte

--viewer, -w

Exécuter la visionneuse de livre numérique en mode débogage

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibre-server

calibre-server

calibre-server [options] [path to library folder...]

Démarre le Serveur de contenu calibre. Le Serveur de contenu calibre expose vos bibliothèques calibre sur internet. Vous pouvez spécifier le chemin vers les dossier de bibliothèque comme argument à calibre-server. Si vous ne spécifiez aucun chemin, toutes les bibliothèques connues par calibre seront utilisées.

Chaque fois que vous passez à calibre-server des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--access-log

Chemin vers le fichier journal d'accès. Ce journal contient des informations au sujet des clients se connectant au serveur et faisant des requêtes. Par défaut aucun accès connecté n'est fini.

--ajax-timeout

Délai d’attente d'une réponse du serveur (en secondes) lors d'une requête.

--auth-mode

Choisir le type d'authentification utilisé. Paramétrer le mode d'authentification HTTP utilisé par le serveur. Paramétrer à "basic" si vous mettez le serveur derrière un proxy SSL. Autrement, laisser à "auto", lequel utilisera "basic" si SSL est configuré sinon il utilisera "digest".

--auto-reload

Recharge automatiquement le serveur lors de changements du code source. Utile pour le développement. Vous devriez aussi spécifier une valeur basse pour la temporisation d'arrêt.

--ban-after

Nombre d'échecs de connexion pour bannir. Le nombre d'échecs de connexion après lesquels une adresse IP est bannie

--ban-for

Bannir l'adresse IP qui a des échecs répétés de connexion. Bannir temporairement l'accès aux adresses IP qui ont des échecs répétés de connexion pour le nombre spécifié de minutes. Utile pour empêcher des tentatives sur des mots de passe invités. Si défini à zéro, aucun bannissement n'est effectué.

--book-list-mode

Choisir le mode liste de livres par défaut. Définir le mode liste de livres par défaut qui sera utilisé pour les nouveaux utilisateurs. Chaque utilisateur peut remplacer la valeur par défaut dans ses propres paramètres. La valeur par défaut est d'utiliser une grille de couverture.

--compress-min-size

Taille minimale pour laquelle les réponses utilisent la compression de données (en bytes).

--custom-list-template

Chemin vers un fichier JSON contenant un modèle pour le mode modèle de liste de livres. La manière la plus simple de créer un tel modèle est d'aller dans Préférences->Partager sur le net->Modèle de liste de livres, créer un modèle et l'exporter.

--daemonize

Exécute le processus en tâche de fond comme un daemon (Linux uniquement)

--displayed-fields

Restreindre l'affichage des champs définis par l'utilisateur. Liste de champs de métadonnées définis par l'utilisateur séparée par des virgules qui seront montrés par le Serveur de contenu dans les affichages /opds et /mobile. Si vous spécifiez cette option, tous les champs qui ne sont pas dans cette liste ne seront pas affichés. Par exemple : my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

Pré-allocation d'un socket, par exemple, avec activation du socket systemd. Par défaut, cette option est activée.

--enable-auth, --disable-auth

Authentification basée sur un mot de passe pour accéder au serveur. Normalement, le serveur est sans restriction, permettant à n'importe qui d'y accéder. Vous pouvez restreindre l'accès à des utilisateurs prédéfinis avec cette option. Par défaut, cette option est désactivée.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

Retour à l'auto-détection de l'interface. Si pour une raison quelconque le serveur est incapable de se lier à l'interface dans l'option listen_on, alors il essayera de détecter une interface qui se connecte avec le monde extérieur et se liera à celle-là. Par défaut, cette option est activée.

--enable-local-write, --disable-local-write

Autoriser les connexions locales non authentifiées à apporter des modifications. Normalement, si vous n'activez pas l'authentification, le serveur fonctionne en mode lecture seule afin d’empêcher les utilisateurs anonymes d'apporter des modifications à vos bibliothèques calibre. Cette option permet aux utilisateurs se connectant à partir de l'ordinateur où le serveur s'exécute d'effectuer des modifications. Ceci permet d'exécuter le serveur sans authentification tout en utilisant calibredb pour apporter des modifications à vos bibliothèques calibre. Notez que l'activation de cette option signifie que tout programme exécuté sur l'ordinateur peut modifier vos bibliothèques calibre. Par défaut, cette option est désactivée.

--enable-log-not-found, --disable-log-not-found

Enregistrer les requêtes HTTP 404 (Not Found). Normallement, le serveur inscrit toutes les requêtes HTTP pour les ressources qui ne sont pas trouvées. Ceci peut occasionné des inscriptions indésirables, si votre serveur est ciblé par des robots, Utiliser cette option pour l'arrêter. Par défaut, cette option est activée.

--enable-use-bonjour, --disable-use-bonjour

Annoncer les flux OPDS via BonJour. Annoncer les flux OPDS via le service BonJour, faisant que les applications de lecture basées OPDS puissent détecter et se connecter automatiquement au serveur. Par défaut, cette option est activée.

--enable-use-sendfile, --disable-use-sendfile

Zéro copie de fichiers de transfert pour augmenter la performance. Ceci utilisera zero-copy dans les transferts in-kernel lors de l'envoi de fichiers sur le réseau, augmentant les performances. Cependant, il peut causer la corruption de transferts de fichiers sur certains systèmes de fichier endommagés. Si vous faites l'expérience de transferts de fichiers corrompus, désactivez-le. Par défaut, cette option est activée.

--help, -h

affiche ce message d'aide et quitte

--ignored-fields

Champs de métadonnées définis par l'utilisateur ignorés. Liste de champs de métadonnées définis par l'utilisateur séparés par des virgules qui ne seront pas montrés par le Serveur de contenu dans les affichages /opds et /mobile. Par exemple : my_rating,my_tags

--listen-on

L'interface sur laquelle écouter pour les connexions. Le comportement par défaut est d'écouter sur toutes les interfaces IPv6 et IPv4 disponibles. Vous pouvez changer cela en, par exemple, "127.0.0.1" pour écouter uniquement des connections IPv4 à partir de la machine locale, ou en "0.0.0.0" pour toutes les connexions IPv4 entrantes.

--log

Chemin vers le fichier journal pour le journal du serveur. Ce journal contient les informations et les erreurs serveur, pas les journaux d'accès. Il est écrit par défaut sur stdout.

--manage-users

Gérer la base de données des utilisateurs autorisés à se connecter à ce serveur. Vous pouvez l'utiliser en mode automatique en ajoutant un –. Voir calibre-server --manage-users -- help pour plus de détails. Voir aussi l'option --userdb.

--max-header-line-size

Taille max. d'une seule en-tête HTTP (en KB).

--max-job-time

Temps maximum pour les processus de travail. Quantité maximale de temps que les processus de travail sont autorisés à s'exécuter (en minutes). Régler à zéro pour aucune limite.

--max-jobs

Nombre maximum de processus de travail. Les processus de travail sont lancés en cas d'utilité et utilisés pour de volumineux travaux comme préparer un livre à la visualisation, ajouter des livres, convertir, etc. Normalement, le nombre maximal de tels processus est basé sur le nombre de cœurs du CPU. Vous pouvez le contrôler par ce réglage.

--max-log-size

Taille max. du fichier journal (en MB). La taille maximale des fichiers journaux, générés par le serveur. Quand le journal devient plus grand que cette taille, il est automatiquement remplacé. Régler à zéro pour désactiver la rotation de journal.

--max-opds-items

Nombre maximum de livres dans les flux OPDS. Le nombre maximum de livres que serveur renverra dans un seul flux d'acquisition OPDS.

--max-opds-ungrouped-items

Le nombre maximum d'éléments dégroupés dans les flux OPDS. Groupe les éléments en catégories telles que auteur/étiquettes à l’aide de la première lettre quand il y a plus que ce nombre d’articles. Réglez à zéro pour désactiver.

--max-request-body-size

Taille max. allouée pour les fichiers téléchargés sur le serveur (en MB).

--num-per-page

Nombre de livres à afficher sur une seule page. Le nombre de livres à afficher sur une seule page dans le navigateur.

--pidfile

Écrire le PID du processus dans le fichier spécifié

--port

Le port sur lequel écouter pour les connexions.

--search-the-net-urls

Chemin d'accès à un fichier JSON contenant les URL de la fonction "Rechercher sur Internet". La façon la plus simple de créer un tel fichier est d'aller dans Préférences-> Partager sur le net->Rechercher sur Internet dans calibre, créer les URLs et les exporter.

--shutdown-timeout

Temps total en secondes à attendre pour un arrêt approprié.

--ssl-certfile

Chemin vers le fichier de certificat SSL.

--ssl-keyfile

Chemin vers le fichier de clé privée SSL.

--timeout

Temps (en secondes) après lequel une connexion inactive est fermée.

--trusted-ips

Autoriser les connexions non authentifiées à partir d'adresses IP spécifiques pour effectuer des modifications. Normalement, si vous n'activez pas l'authentification, le serveur fonctionne en mode lecture seule, afin de ne pas permettre à des utilisateurs anonymes d'apporter des modifications à vos bibliothèques calibre. Cette option permet à toute personne se connectant à partir des adresses IP spécifiées d'effectuer des modifications. Il doit s'agir d'une liste d'adresses ou de spécifications de réseau séparées par des virgules. Cette option est utile si vous voulez faire fonctionner le serveur sans authentification mais que vous utilisez tout de même calibredb pour modifier vos bibliothèques de calibre. Notez que l'activation de cette option signifie que toute personne se connectant à partir des adresses IP spécifiées peut apporter des modifications à vos bibliothèques calibre.

--url-prefix

Un préfixe à ajouter à toutes les URLs. Utile si vous souhaitez exécuter ce serveur derrière un proxy inversé. Par exemple, utilisez /calibre comme préfixe d'URL.

--userdb

Chemin vers la base de données utilisateur à utiliser pour l'authentification. La base de données est un fichier SQLite. Pour le créer utilisez --manage-users. Vous pouvez en lire plus à propos de la gestion des utilisateurs sur : https://manual.calibre-ebook.com/fr/server.html#managing-user-accounts-from-the-command-line-only

--version

affiche le numéro de version du logiciel et quitte

--worker-count

Nombre de processus de travail utilisés pour effectuer les requêtes.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibre-smtp

calibre-smtp

calibre-smtp [options] [from to text]

Envoyer des courriels en utilisant le protocole SMTP. calibre-smtp a deux modes d’opération. Dans le mode composer vous spécifiez l’expéditeur, le destinataire et le texte, ceux-ci sont utilisés pour élaborer et envoyer un message électronique. Dans le mode filtre, calibre-smtp lit un message électronique complet depuis STDIN et l’envoie.

le texte est le corps du message.
Si le texte n’est pas spécifié, un message complet est lu depuis STDIN.
de est l’adresse courriel de l’expéditeur et à est l’adresse courriel du destinataire. Quand un courriel complet est lu depuis STDIN, de et à sont uniquement utilisés dans la négociation SMTP, l’en-tête de message n’est pas modifié.

Chaque fois que vous passez à calibre-smtp des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--fork, -f

Redirige et délivre le message en tâche de fond. Si vous utilisez cette option, vous devriez aussi aussi utiliser --outbox pour traiter les erreurs de livraison.

--help, -h

affiche ce message d'aide et quitte

--localhost, -l

Nom d'hôte de l'hôte local. Utilisé lors d'une connexion à un serveur SMTP.

--outbox, -o

Chemin vers le dossier maildir où enregistrer les courriels qui ont échoué.

--timeout, -t

Délai d'attente pour la connexion

--verbose, -v

Soyez plus détaillé

--version

affiche le numéro de version du logiciel et quitte

COMPOSE MAIL

Options pour rédiger un courriel. Ignoré si le texte n’est pas spécifié

--attachment, -a

Fichier à attacher au courriel

--subject, -s

Sujet du courriel

SMTP RELAY

Options pour utiliser un serveur de relais SMTP pour envoyer un courriel. calibre essayera d’envoyer un courriel directement à moins que –relay est spécifié.

--cafile

Chemin d'accès à un fichier de certificats CA concaténés au format PEM, utilisé pour vérifier le certificat du serveur lors de l'utilisation de TLS. Par défaut, les certificats CA du système sont utilisés.

--dont-verify-server-certificate

Ne vérifiez pas le certificat de serveur lors de la connexion à l'aide de TLS. C'était le comportement par défaut dans les versions de calibre antérieures à 3.27. Si vous utilisez un relais avec un certificat auto-signé ou autre certificat invalide, vous pouvez utiliser cette option pour restaurer le comportement d'avant la version 3.27.

--encryption-method, -e

Méthode de cryptage à utiliser lors de la connection à un relais. Les choix sont TLS, SSL et AUCUN. L'option par défaut est TLS. ATTENTION : choisir AUCUN est très risqué

--password, -p

Mot de passe pour le relais

--port

Port pour se connecter à un serveur relais. L'option par défaut est d'utiliser le 465 si la méthode de cryptage est le SSL et 25 autrement.

--relay, -r

Un serveur de relais SMTP à utiliser pour envoyer un courriel.

--username, -u

Nom d'utilisateur pour le relais

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- calibredb

calibredb

calibredb command [options] [arguments]

calibredb est l’interface en ligne de commande de la base de données de calibre. Elle a plusieurs sous-commandes, documentées ci-dessous.

La commande :calibredb peut être utilisée pour manipuler soit une base de données de calibre spécifiée par le chemin ou calibre Serveur de contenu fonctionnant soit en local soit sur Internet. Vous pouvez lancer calibre Serveur de contenu en utilisant soit la commande calibre-server.
ou dans le programme principal de calibre, cliquer sur Connecter/partager  →
Démarrer le serveur de contenu. Puisque calibredb peut apporter des modifications à vos bibliothèques vous devez d’abord configurer l’authentification sur le serveur. Là il y a deux façons de le faire :

- Si vous prévoyez de vous connecter uniquement à un serveur fonctionnant sur le même ordinateur,

vous pouvez simplement utiliser l’option « –enable-local-write » du Serveur de contenu pour permettre à tout programme, y compris calibredb, de fonctionner sur l’ordinateur local pour apporter des modifications aux données de calibre. Lors de l’exécution le serveur du programme principal de calibre, cette option se trouve sous Préférences → Partage sur le net → Avancé.

- Si vous souhaitez activer l’accès à Internet, vous devez le configurer

sur le serveur et utilisez les options --username et --password.
pour donner accès à calibredb. Vous pouvez configurer l’authentification de l’utilisateur pour calibre-server en utilisant la commande --enable-auth.
et en utilisant l’option « –manage-users » pour créer les comptes utilisateurs.
Si vous exécutez le serveur à partir du programme principal de calibre, utilisez l’option Préférences → Partage sur le net → Nom d’utilisateur/mot de passe requis.

Pour vous connecter à un serveur de contenu en cours d’exécution, passez l’URL du serveur l’option --with-library, voir la documentation de cette option pour des détails et exemples.

- OPTIONS GLOBALES

- list

- add

- AJOUTER A PARTIR DES DOSSIERS

- remove

- add_format

- remove_format

- show_metadata

- set_metadata

- export

- catalog

- EPUB OPTIONS

- 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

OPTIONS GLOBALES

--help, -h

affiche ce message d'aide et quitte

--library-path, --with-library

Chemin vers la bibliothèque calibre. Par défaut utilise le chemin enregistré dans les paramètres. Vous pouvez vous connecter à un Serveur de contenu calibre pour effectuer des actions sur des bibliothèques distantes. Pour faire cela utiliser une URL de la forme : http://hostname:port/#library_id par exemple, http://localhost:8080/#mylibrary. library_id est l'identifiant de la bibliothèque à laquelle vous vous voulez cous connecter sur le Serveur de Contenu. Vous pouvez utiliser la la valeur spéciale library_id - pour obtenir une liste des bibliothèques disponibles sur le serveur. Pour plus de détails sur comment paramétrer l'accès via un Serveur de Contenu, voir https://manual.calibre-ebook.com/fr/generated/fr/calibredb.html.

--password

Mot de passe pour la connexion à un Serveur de contenu calibre. Pour lire le mot de passe en entrée standard, utilisez la valeur spéciale : <stdin>. Pour lire le mot de passe à partir d'un fichier, utilisez : <f:/path/to/file>. (c.a.d <f: suivi du chemin complet du fichier et ajouter >). Les chevrons (< ou >) ci-dessus sont obligatoires, n'oubliez pas de les échapper ou d'utiliser des guillemets pour votre shell.

--timeout

Le délai d'attente, en secondes, lors de la connexion à une bibliothèque calibre sur le réseau. La valeur par défaut est de deux minutes.

--username

Nom d'utilisateur pour la connexion au Serveur de contenu calibre

--version

affiche le numéro de version du logiciel et quitte

list

calibredb list [options]

Lister les livres disponibles dans la base Calibre.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--ascending

Trier les résultats dans l’ordre ascendant

--fields, -f

Les champs à afficher quand on liste les livres dans la base de données. Devrait être une liste de champs séparés par une virgule. Champs disponibles : author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Par défaut : title,authors. Le champ spécial "all" peut être utiliser pour sélectionner tous les champs. En supplément des champs intégrés ci-dessous, les champs personnalisés sont également disponibles en tant que *field_name, par exemple, pour un champ personnalisé #notation, utiliser le nom : *notation

--for-machine

Générer une sortie au format JSON, qui est plus commode pour une analyse machine. Entraîne les options largeur de ligne et séparateur à être ignorés.

--limit

Le maximum de résultats à afficher. Par défaut : tous

--line-width, -w

Largeur maximum pour une seule ligne en sortie. Par défaut détecte la taille de l’écran.

--prefix

Préfixe pour tous les chemins vers des fichiers. Par défaut : chemin absolu du dossier de la bibliothèque.

--search, -s

Filtrer les résultats à l’aide de la requête de recherche. Pour le format de la requête de recherche, veuillez consulter la section recherche dans le manuel utilisateur. Par défaut n’effectue aucun filtrage

--separator

La chaîne utilisée pour séparer des champs. Par défaut : un espace

--sort-by

Le champ par lequel les résultats doivent être triés. Vous pouvez spécifier plusieurs champs en les séparant par des virgules. Champs disponibles : author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Défaut : id

--template

Le modèle à exécuter si "template" figure dans la liste des champs. Default : None

--template_file, -t

Chemin d'accès à un fichier contenant le modèle à exécuter si "template" figure dans la liste des champs. Default : None

--template_heading

Intitulé de la colonne du modèle. Valeur par défaut : template. Cette option est ignorée si l'option --for-machine est définie.

add

calibredb add [options] fichier1 fichier2 fichier3 …

Ajouter les fichiers spécifiés comme livre numérique à la base de données. Vous pouvez aussi spécifier des dossiers, voir ci-dessous les options relatives aux dossiers.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--authors, -a

Définissez les auteurs du(es) livre(s) ajouté(s)

--automerge, -m

Si des livres dont les titres et les auteurs sont similaires sont trouvés, fusionnez automatiquement les formats (fichiers) entrants dans les fiches de livres existantes. Une valeur "ignore" signifie que les formats en double sont éliminés. Une valeur "overwrite" signifie que les formats en double dans la bibliothèque sont écrasés avec les fichiers nouvellement ajoutés. Une valeur de "new_record" signifie que les formats dupliqués sont placés dans une nouvelle fiche de livre.

--cover, -c

Chemin à utiliser pour la couverture du livre ajouté

--duplicates, -d

Ajouter des livres à la base de données même s'ils existent déjà. La comparaison se fait sur la base des titres des livres et des auteurs. Notez que l'option --automerge est prioritaire.

--empty, -e

Ajouter un livre vide (un livre sans formats)

--identifier, -I

Paramètres les identifiants pour ce livre, par ex. -I asin:XXX -I isbn:YYY

--isbn, -i

Définir l'ISBN du(es) livre(s) ajouté(s)

--languages, -l

Une liste de langages séparés par une virgule (mieux d'utiliser les codes de langue IS0639, ainsi certains noms de langues peuvent aussi être reconnus)

--series, -s

Définir les séries du(es) livre(s) ajouté(s)

--series-index, -S

Assigne le numéro de la série de(s) livre(s) ajoutés

--tags, -T

Définir les étiquettes du(es) livre(s) ajouté(s)

--title, -t

Définir le titre du(es) livre(s) ajouté(s)

AJOUTER A PARTIR DES DOSSIERS

Options pour contrôler l’ajout de livres à partir de dossiers. Par défaut seuls les fichiers qui ont des extensions de type livre numérique connues sont ajoutés.

--add

Un motif de nom de fichier (glob), les fichiers correspondant à ce motif seront ajoutés lors de la recherche de fichiers dans les dossiers, même s'ils ne sont pas d'un type de fichier de livre numérique connu. Peut être spécifié plusieurs fois pour plusieurs motifs.

--ignore

Un motif de nom de fichier (glob), les fichiers correspondant à ce motif seront ignorés lors de la recherche de fichiers dans les dossiers. Peut être spécifié plusieurs fois pour plusieurs motifs. Par exemple : *.pdf ignorera tous les fichiers PDF

--one-book-per-directory, -1

Suppose que chaque dossier dispose d’un seul livre logique et que tous les fichiers dans celui-ci sont des formats différents de ce même livre.

--recurse, -r

Traite les dossiers récursivement

remove

calibredb supprime les ids

Supprime les livres identifiés par leurs ids de la base de données. Les ids devraient être une liste de nombres id séparés par des virgules (vous pouvez obtenir les numéros id en utilisant la commande search).
Par exemple, 23,34,57-85 (quand un intervalle est précisé, le dernier nombre dans l’intervalle n’est pas inclus).

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--permanent

N'utilisez pas les Corbeille

add_format

calibredb add_format [options] id ebook_file

Ajouter le livre numérique dans ebook_file aux formats disponibles pour le livre logique identifié par l’id. Vous pouvez obtenir l’id à l’aide de la commande search. Si le format existe déjà, il est remplacé, à moins que l’option ne pas remplacer est spécifiée.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--as-extra-data-file

Ajouter le fichier en tant que fichier de données supplémentaire au e-livre, et non en tant que format ebook

--dont-replace

Ne pas remplacer le format s'il existe déjà

remove_format

calibredb remove_format [options] id fmt

Supprime le format fmt du livre logique identifié par l’id. Vous pouvez obtenir l’identifiant à l’aide de la commande search. fmt devrait être une extension de fichier comme LRF, TXT ou EPUB. Si le fichier logique n’a pas de format fmt disponible, aucune action n’est effectuée.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

show_metadata

calibredb show_metadata [options] id

Afficher les métadonnées stockées dans la base de données calibre pour le livre identifié par l’id.
id est un identifiant obtenu à l’aide de la commande search.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--as-opf

Imprimer les métadonnées dans un formulaire OPF (XML)

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

Place les métadonnées stockées dans la base de données de calibre pour le livre identifié par

book_id à partir du fichier OPF metadata.opf. book_id est un numéro d’id issu de la commande rechercher. Vous pouvez obtenir un aperçu rapide du format OPF à

l’aide du commutateur –as-opf de la commande show_metadata. Vous pouvez également placer les métadonnées de champs individuels avec l’option –field. Si vous utilisez l’option –field, il n’est pas nécessaire de spécifier un fichier OPF.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--field, -f

Le champ à définir. Le format est nom_champ:valeur, par exemple: --field tags:tag1,tag2. Utilisez --list-fields pour récupérer une liste de tous les noms de champs. Vous pouvez spécifier cette option plusieurs fois pour définir plusieurs champs. Note: pour le champs "languages" (langues), vous devez utiliser les codes de langues ISO639 (par exemple, en pour Anglais, fr pour Français, et ainsi de suite). Pour les identifiants (identifiers), la syntaxe est --field identifiers:isbn:XXXX,doi:YYYYY. Pour les champs booléens (oui/non), utilisez "true" (vrai) ou "false" (faux) ou "yes" (oui) et "no" (non).

--list-fields, -l

Lister les champs de métadonnées pouvant être utilisés avec l'option --field

export

calibredb export [options] ids

Exporter les livres spécifiés par ids (une liste séparée par des virgules) vers le système de fichiers.
L’opération d’export enregistre tous les formats du livre, la couverture et les métadonnées (dans n’importe quel un fichier OPF). Tous les fichiers de données supplémentaires associés au livre sont également enregistrés.
Vous pouvez obtenir les numéros id à partir de la commande search.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--all

Exporter tous les livres dans la base, en ignorant la liste des ids.

--dont-asciiize

Faire en sorte que le calibre convertisse tous les caractères non anglais en équivalents anglais pour les noms de fichiers. Ceci est utile si vous enregistrez sur un ancien système de fichiers qui ne prend pas entièrement en charge les noms de fichiers Unicode. Spécifier cette bascule désactivera ce comportement.

--dont-save-cover

Normalement, calibre enregistrera la couverture dans un fichier séparé avec les fichiers du livre numérique. Spécifier cette bascule désactivera ce comportement.

--dont-save-extra-files

Enregistrez tous les fichiers de données associés au livre lors de l'enregistrement du livre Spécifier cette bascule désactivera ce comportement.

--dont-update-metadata

En temps normal calibre mettra à jour les métadonnées des fichiers sauvegardés à partir du contenu de la bibliothèque calibre. L’enregistrement prendra davantage de temps. Spécifier cette bascule désactivera ce comportement.

--dont-write-opf

Normalement, calibre écrira les métadonnées dans un fichier OPF séparé avec les fichiers de livre numérique actuels. Spécifier cette bascule désactivera ce comportement.

--formats

Liste séparée par des virgules de formats à enregistrer pour chaque livre. Par défaut tous les formats disponibles sont sauvegardés.

--progress

Signaler l'avancement

--replace-whitespace

Remplacer les espaces par des underscores.

--single-dir

Exporter tous les livres vers un dossier unique

--template

Modèle pour contrôler le nom des fichier et la structure des dossiers d’enregistrement des fichiers. Par défaut : "{author_sort}/{title}/{title} - {authors}" qui va sauvegarder les livres dans un sous-dossier par auteur avec des noms de fichiers contenant le titre et l’auteur. Les variables disponibles sont  : {author_sort, authors, id, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

Le format suivant lequel afficher les dates. %d - jour, %b - mois, %m - numéro de mois, %Y - année. Par défaut : %b, %Y

--to-dir

Exporter les livres vers le dossier spécifié. Par défaut : .

--to-lowercase

Convertir les chemins en minuscule.

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.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--ids, -i

Liste séparée par des virgules d’identifiants de base de données à cataloguer. Si déclaré, --search est ignoré. Par défaut : all

--search, -s

Filtrer les résultats par la requête de recherche. Pour le format de la requête de recherche, veuillez consulter la section recherche dans le Manuel Utilisateur. Par défaut: Pas de filtre

--verbose, -v

Affiche des informations détaillées. Utile pour le débogage

EPUB OPTIONS

--catalog-title

Titre du catalogue généré utilisé comme titre dans les métadonnées. Par défaut : 'My Books' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--cross-reference-authors

Crée des références croisées dans la section Auteurs pour les livres avec des auteurs multiples. Par défaut: 'False' S'applique aux formats de sortie: AZW3, EPUB et MOBI

--debug-pipeline

Enregistre la sortie à différentes étapes du processus de conversion vers le dossier spécifié. Utile si vous n'êtes pas sûr de l'étape de la conversion à laquelle se produit le bogue. Par défaut : 'None' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--exclude-genre

Expression régulière décrivant les étiquettes à exclure comme genres. Par défaut : '[.+]|^+$' exclut les étiquettes entre crochets, par ex. '[Project Gutenberg]', et '+', l'étiquette par défaut pour les livres lus. S’applique aux formats de sortie : AZW3, EPUB, MOBI

--exclusion-rules

Indique les règles à appliquer pour exclure des livres du catalogue généré. Le modèle pour une règle d'exclusion est ('<nom de la règle>','Étiquettes','<liste d'étiquettes séparées par des virgules>') or ('<nom de la règle>','<nom de colonne>','<valeur>'). Par exemple: (('Livres archivés','#statut','Archivé'),) exclura les livres qui ont la valeur 'Archivé' dans la colonne 'statut'. Quand plusieurs règles sont définies, toutes les règles seront appliquées. Par défaut: "(('Catalogs','Tags','Catalog'),)" S'applique aux formats de sortie AZW3, EPUB, MOBI

--generate-authors

Inclut la section 'Auteurs' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--generate-descriptions

Inclut la section 'Descriptions' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--generate-genres

Inclut la section 'Genres' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--generate-recently-added

Inclut la section 'Ajoutés récemment' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--generate-series

Inclut la section 'Series' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--generate-titles

Inclut la section 'Titres' dans le catalogue. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--genre-source-field

Champ source pour la section 'Genres'. Par défaut : 'Étiquettes' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--header-note-source-field

Champ personnalisé contenant une note de texte à insérer dans l'en-tête de Description. Par défaut : '' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--merge-comments-rule

#<custom field> : spécifier [before|after]:[True|False] : <custom field> Champ personnalisé contenant des notes à fusionner avec les commentaires [before|after] Placement des notes par rapport au commentaires [True|False] - Une ligne horizontale est insérée entre les notes et les commentaires Par défaut : '::' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--output-profile

Spécifie le profil de sortie. Dans certains cas, un profil de sortie est requis pour optimiser le rendu en fonction du périphérique. Par exemple, 'kindle' ou 'kindle_dx' créent une table des matières structurée avec des sections et des articles. Par défaut: 'None' S'applique aux formats de sortie: AZW3, EPUB, MOBI

--prefix-rules

Indique les règles utilisées pour inclure des préfixes qui indiquent qu'un livre est lu, qu'il est sur votre liste d'envies et d'autres indications personnalisées. Le modèle pour une règle de préfixe est: ('<rule name>','<source field>','<pattern>','<prefix>'). Quand plusieurs règles sont définies, toutes les règles s'appliquent. Par défaut: "(('Read books','tags','+','✓'),('Wishlist item','tags','Wishlist','×'))" S'applique aux formats de sortie AZW3, EPUB, MOBI

--preset

Employe un préréglage appelé créé avec le constructeur de Catalogue GUI. Un préréglage spécifie tous les paramètres pour construire un catalogue. Par défaut: 'None' S'applique aux formats de sortie AZW3, EPUB, MOBI

--thumb-width

Indice de taille (en pouces) pour les couvertures de livres dans le catalogue. Gamme : 1.0 - 2.0 Par défaut : '1.0' S’applique aux formats de sortie : AZW3, EPUB, MOBI

--use-existing-cover

Remplace la couverture existante lorsque le catalogue est généré. Par défaut : 'False' S’applique aux formats de sortie : AZW3, EPUB, MOBI

saved_searches

calibredb saved_searches [options] (list|add|remove)

Gère les recherches sauvegardées stockées dans la base de données.
Si vous essayez d’ajouter une requête avec un nom préexistant, elle sera remplacée.

Syntaxe pour l’ajout :

calibredb saved_searches add search_name search_expression

Syntaxe pour la suppression :

calibredb saved_searches remove search_name

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

add_custom_column

calibredb add_custom_column [options] label name datatype

Créer une colonne personnalisée. label correspond au nom de la colonne pour le programme.
Ne doit pas contenir d’espace ou de deux-points. name est le nom de la colonne pour l’utilisateur.
datatype est à choisir parmi : bool, comments, composite, datetime, enumeration, float, int, rating, series, text

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--display

Un dictionnaire des options pour personnaliser la manière dont les données dans cette colonne seront interprétées. C'est une chaîne JSON. Pour des colonnes d'énumération, employez --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" Il y a beaucoup d'options qui peuvent entrer dans la variable d'affichage.Les options par type de colonne sont : 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_decorations La meilleure manière de trouver des combinaisons légitimes est de créer dans le GUI une colonne personnalisée du type approprié et d'ensuite de regarder dans la sauvegarde de l'OPF d'un livre (assurez-vous qu'un nouvel OPF a été créé depuis que la colonne a été ajoutée). Vous verrez le JSON pour l' « affichage » pour la nouvelle colonne dans l'OPF.

--is-multiple

Cette colonne stocke les étiquettes comme des données (par exemple des valeurs multiples séparées par des virgules). Appliqué seulement si le type de donnée est du texte.

custom_columns

calibredb custom_columns [options]

Liste des colonnes personnalisées disponibles. Affiche les entêtes de colonne et les ids.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--details, -d

Afficher les détails pour chaque colonne.

remove_custom_column

calibredb remove_custom_column [options] label

Enlever la colonne personnalisée identifiée par le libellé. Vous pouvez afficher
les colonnes disponibles avec la commande custom_columns.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--force, -f

Ne pas demander de confirmation

set_custom

calibredb set_custom [options] valeur de l’id de la colonne

Définir la valeur d’une colonne personnalisée pour le livre identifié par l’id.
Vous pouvez obtenir une liste d’ids en utilisant la commande search.
Vous pouvez obtenir une liste des noms de colonnes personnalisés en utilisant la commande custom_columns.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--append, -a

Si la colonne stocke des valeurs multiples, ajouter les valeurs spécifiées à celles existantes, au lieu de les remplacer.

restore_database

calibredb restore_database [options]

Restaure la base de données à partir des métadonnées stockées dans les fichiers OPF présents dans chaque dossier de la bibliothèque calibre.
Ceci est utile si votre fichier metadata.db a été corrompu.

ATTENTION : cette commande régénère complètement votre base de données.
Vous perdrez toutes les recherches sauvegardées, les catégories utilisateur, les tableaux de connexions, les réglages de conversion par livre et les recettes personnalisées.
Les métadonnées restaurées seront aussi précises que ce qui sera trouvé dans les fichiers OPF.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--really-do-it, -r

Effectuer réellement la récupération. La commande ne sera pas exécutée si cette option n’est pas spécifiée.

check_library

calibredb check_library [options]

Réalise des vérifications sur le système de fichiers représentant une bibliothèque. Les rapports sont invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--csv, -c

Sortie en CSV

--ignore_extensions, -e

Liste d’extensions à ignorer séparées par des virgules. Par défaut: toutes

--ignore_names, -n

Liste de noms à ignorer séparés par des virgules. Par défaut : tous

--report, -r

Liste de rapports séparés par des virgules. Par défaut : tous

--vacuum-fts-db

Nettoyer la base de données de recherche en texte intégral. Cette opération peut être très lente et gourmande en mémoire, en fonction de la taille de la base de données.

list_categories

calibredb list_categories [options]

Produit un rapport sur les informations des catégories dans la base de données.
Les informations sont les équivalents de ce qui est affiché dans le Navigateur d’étiquettes.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--categories, -r

Liste de catégories de noms de recherche séparées par des virgules. Par défaut : tous

--csv, -c

Sortie en CSV

--dialect

Le type de fichier CSV à produire. Choix : excel, excel-tab, unix

--item_count, -i

Sort seulement le nombre d’articles dans une catégorie au lieu du nombre d’occurrences par article à l’intérieur de la catégorie

--width, -w

Largeur maximum pour une seule ligne en sortie. Par défaut détecte la taille de l’écran.

backup_metadata

calibredb backup_metadata [options]

Sauvegarde les métadonnées stockées dans la base de données dans les fichiers OPF individuels dans chaque dossier de livres. Ceci se fait normalement automatiquement, mais vous pouvez lancer cette commande pour forcer la régénération des fichiers OPF, avec l’option –all.

Notez qu’il y a normalement aucun besoin de faire ceci, car les fichiers OPF sont mis à jour automatiquement, chaque fois que les métadonnées sont changées.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--all

Normalement, cette commande opère seulement sur les livres qui ont des fichiers OPF périmés. Cette option la fait opérer sur tous les livres.

clone

calibredb clone path/to/new/library

Crée un clone de la bibliothèque active. Vous obtiendrez une bibliothèque vide qui a exactement les même colonnes personnalisées, Bibliothèques virtuelles et autres paramétrages de la bibliothèque active.

Cette bibliothèque clonée ne contiendra aucun livre. Si vous voulez avoir une copie complète, contenu inclus, le plus simple est de copier manuellement le dossier contenant cette bibliothèque.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

embed_metadata

calibredb embed_metadata [options] book_id

Met à jour les métadonnées dans les fichiers de livre stockées dans la bibliothèque calibre depuis les métadonnées de la base de données calibre. Normalement, les métadonnées sont mises à jour uniquement quand des fichiers sont exportés depuis calibre, cette commande est utile si vous voulez que les fichiers soient mis à jour sur place. Notez que les différents formats de fichiers supportent différentes quantités de métadonnées. Vous pouvez utiliser la valeur spéciale “all” de book_id pour mettre à jour les métadonnées dans tous les livres. Vous pouvez aussi spécifier beaucoup d’ids de livres séparés par des espaces et des ensembles d’ids séparés par des traits d’union : calibredb embed_metadata 1 2 10-15 23

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--only-formats, -f

Met uniquement à jour les métadonnées dans les fichiers au format spécifié. Spécifier le plusieurs fois pour de multiples formats. Par défaut, tous les formats sont mis à jour.

search

calibredb search [options] expression de recherche

Recherche dans la bibliothèque après le terme spécifié dans la recherche, renvoie une liste séparée par une virgule de livres dont les ids correspondent à l’expression de recherche. Le format de sortie est utilisé pour alimenter d’autres commandes qui acceptent une liste d’ids comme entrée.

L’expression de recherche peut être n’importe quoi à partir du puissant langage de requête de recherche de calibre, par exemple : calibredb search author:asimov “title: »i robot »”

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--limit, -l

Le nombre maximum de résultats à renvoyer. Par défaut tous les résultats

fts_index

calibredb fts_index [options] enable/disable/status/reindex

Contrôle le processus d’indexation de la recherche intégrale du texte.

enable
Active l’indexation RIT disable
Désactive l’indexation RIT pour cette bibliothèque.
status
Affiche le statut actuel de l’indexation reindex
Peut être utilisé pour réindexer soit des livres particuliers ou la bibliothèque entière. Pour réindexer des livres particuliers spécifiez les identifiants des livres comme arguments supplémentaires après la commande reindex. Si aucun identifiant de livre n’est spécifié, la bibliothèque entière est réindexée.

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--indexing-speed

La vitesse d'indexation. Utiliser rapide pour une indexation rapide utilisant toutes les ressources de votre ordinateur et lent pour une indexation moins gourmande en ressources. Noter que la vitesse est réinitialisée à lent après chaque demande.

--wait-for-completion

Attendre que tous les livres soient indexés, en montrant périodiquement la progression de l'indexation

fts_search

calibredb fts_search [options] expression de recherche

Effectue une recherche intégrale du texte sur l’ensemble de la bibliothèque ou sur un sous-ensemble de celle-ci

Chaque fois que vous passez à calibredb des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

--do-not-match-on-related-words

La correspondance se fait uniquement sur les mots exacts et non sur les mots apparentés. La correction ne correspondra donc pas à corrigé.

--include-snippets

Inclure des extraits du texte entourant chaque correspondance. Noter que cela rend la recherche beaucoup plus lente.

--indexing-threshold

Pourcentage de la bibliothèque qui doit être indexée avant que la recherche ne soit autorisée. La valeur par défaut est 90

--match-end-marker

Le marqueur utilisé pour indiquer la fin d'un mot correspondant à l'intérieur d'un extrait

--match-start-marker

Le marqueur utilisé pour indiquer le début d'un mot correspondant dans un extrait

--output-format

Le format de sortie des résultats de la recherche. Soit "text" pour du texte brut, soit "json" pour une sortie JSON.

--restrict-to

Restreindre les livres recherchés, en utilisant une expression de recherche ou des identifiants. Par exemple : ids:1,2,3 pour restreindre par ids ou search:tag:foo pour restreindre aux livres ayant le tag foo.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [options]

Convertir un livre numérique d’un format à un autre.

input_file est l’entrée output_file est la sortie. Chacun des deux doit être spécifié comme premiers arguments de la commande.

Le format de livre numérique de sortie est défini par l’extension du fichier output_file. output_file peut également être un format spécial .EXT où EXT est l’extension du fichier de sortie. Dans ce cas, le nom du fichier de sortie est dérivé du nom du fichier d’entrée. Notez que les noms de fichier ne doivent pas commencer par un trait d’union. Pour finir, si output_file n’a pas d’extension, il est traité comme un dossier et un «open e-book » (OEB) se composant des fichiers HTML est écrit dans ce dossier. Ces fichiers sont les fichiers qui auraient été normalement traité par l’extension de sortie.

Après avoir spécifié le fichier d’entrée et de sortie vous pouvez adapter la conversion en spécifiant diverses options. Les options disponibles dépendent des types de fichier d’entrée et de sortie. Pour obtenir de l’aide sur celles-ci, spécifiez le fichier d’entrée et de sortie et puis employez l’option -h.

Pour la documentation complète du système de conversion voir Conversion de livre numérique

Chaque fois que vous passez à ebook-convert des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

Les options et les valeurs par défaut pour les options changent selon les formats d’entrée et de sortie, aussi vous devriez toujours vérifier avec

ebook-convert myfile.input_format myfile.output_format -h

Ci-dessous se trouvent les options communes à toutes les conversions, suivies par les options spécifiques à tous les formats d’entrée et de sortie.

- OPTIONS D’ENTRÉE

- OPTIONS DE SORTIE

- ASPECT ET COMPORTEMENT

- TRAITEMENT HEURISTIQUE

- RECHERCHER ET REMPLACER

- DÉTECTION DE LA STRUCTURE

- TABLE DES MATIERES

- METADONNEES

- DÉBOGAGE

--help, -h

affiche ce message d'aide et quitte

--input-profile

Spécifier le profil d’entrée. Ce profil d’entrée indique au système de conversion comment interpréter les diverses informations dans le document d’entrée. Par exemple, la résolution dépend des longueurs. (c.-à-d. longueurs en pixels). Les choix sont : cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

Liste les noms des recettes intégrées. Vous pouvez créer un livre numérique depuis une recette intégrée de la manière suivante : ebook-convert "Nom Recette.recipe" sortie.epub

--output-profile

Spécifiez le profil de sortie. Le profil de sortie explique au système de conversion comment optimiser les documents créés pour un périphérique donné. Dans certains cas, un profil de sortie peut être utilisé pour optimiser la sortie pour un périphérique bien particulier, mais cela est rarement nécessaire. Les choix sont :cybookg3, cybook_opus, default, generic_eink, generic_eink_hd, generic_eink_large, 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

--version

affiche le numéro de version du logiciel et quitte

OPTIONS D’ENTRÉE

Options pour contrôler le traitement du fichier d’entrée mobi

--input-encoding

Spécifier l’encodage des caractères du document d’entrée. Si réglée cette option outrepassera tout encodage déjà déclaré par le document lui-même. Particulièrement utile pour des documents ne déclarant pas d’encodage ou ayant des déclarations d’encodage incorrectes.

OPTIONS DE SORTIE

Options pour contrôler le traitement de la sortie epub

--dont-split-on-page-breaks

Désactiver le sectionnement aux sauts de page. En temps normal, le fichier d’entrée est sectionné à chaque saut de page pour former deux fichiers; vous bénéficiez ainsi d'un livre numérique susceptible d’être analysé plus rapidement et avec moins de ressources. Toutefois, sectionner les fichiers est un processus lent, donc il est recommandé de désactiver le sectionnement si le fichier d’entrée contient de nombreux sauts de page.

--epub-flatten

Cette option est nécessaire si vous avez l’intention d’utiliser l’EPUB avec FBReaderJ. Elle va aplatir la structure du système de fichiers à l’intérieur de l’EPUB, mettant tous les fichiers sur le niveau supérieur.

--epub-inline-toc

Insérer une Table des Matières intégrée qui apparaîtra en tant qu'élément du contenu principal du livre.

--epub-max-image-size

Taille maximale de l'image (largeur x hauteur). Une valeur de none signifie que sera utilisé la taille d'écran du profil de sortie. Une valeur de profile signifie qu'aucune taille maximale n'est spécifiée. Par exemple, une valeur de 100x200 entraînera le redimensionnement de toutes les images de manière à ce que leur largeur ne dépasse pas 100 pixels et leur hauteur 200 pixels. Notez que cela n'affecte que la taille des fichiers d'image eux-mêmes. Une image donnée peut être rendue à une taille différente en fonction du style qui lui est appliqué dans le document.

--epub-toc-at-end

Mettre la Table des Matières intégrée à la fin du livre plutôt qu'au début.

--epub-version

La version du fichier EPUB à générer. EPUB 2 est le plus largement compatible, n'utilisez EPUB 3 que si vous savez que vous en avez réellement besoin.

--extract-to

Extrait le contenu du fichier EPUB généré vers le dossier spécifié. Le contenu du dossier sera d’abord effacé, donc faites attention.

--flow-size

Diviser tous les fichiers HTML plus grand que cette taille (en Ko). Ceci est nécessaire car la plupart des lecteurs EPUB ne prennent pas en charge des fichiers de grande taille. Par défaut 260 Ko est la taille requise par Adobe Digital Editions. Paramétrer à 0 pour désactiver la division basée sur la taille.

--no-default-epub-cover

Normalement, si le fichier d’entrée n’a pas de couverture et que vous n’en avez pas spécifié une, une couverture par défaut est générée avec le titre, les auteurs, etc. Cette option désactive la génération de cette couverture.

--no-svg-cover

Ne pas utiliser le format SVG pour la couverture du livre. Utiliser cette option si votre EPUB va être utilisé sur un périphérique qui ne prend pas en charge SVG, comme l’iPhone ou le JetBook Lite. Sans cette option, ce type périphérique affichera une page blanche comme couverture.

--preserve-cover-aspect-ratio

Lors de l’utilisation d’une image SVG en couverture, cette option va entraîner une mise à l’échelle permettant de couvrir tout l’écran, mais va toujours garder les proportions (ratio hauteur/largeur) de l’image d’origine. Ceci signifie qu’il peut y avoir des bordures blanches sur les cotés, en haut ou en bas de l’image, mais que celle-ci ne sera jamais distordue. Sans cette option l’image peut être légèrement distordue, mais il n’y aura pas de bordures.

--pretty-print

Si spécifié, l’extension de sortie essaiera de créer une sortie qui sera aussi lisible que possible pour un être humain. Peut être sans effet avec certaines extensions de sortie.

--toc-title

Titre pour toutes les Tables des Matières intégrées générées.

ASPECT ET COMPORTEMENT

Options pour contrôler l’apparence de la sortie

--asciiize

Translittère les caractères Unicode en une représentation ASCII. A utiliser avec précaution car cela remplacera les caractères Unicode par des caractères ASCII. Par exemple, elle remplacera "Pelé" par "Pele". Notez également que dans les cas où il existe plusieurs représentations d'un caractère (caractères partagés par le chinois et le japonais par exemple), la représentation basée sur la langue de l'interface actuelle de calibre sera utilisée.

--base-font-size

La taille de base pour la police de caractère en pts. Toutes les tailles de police dans le livre produit seront redimensionnées par rapport à cette taille. En choisissant une taille large, vous pouvez obtenir des polices plus grandes en sortie et vice versa. Par défaut, quand la valeur est zéro, la taille de base pour la police est choisie par rapport au profil de sortie que vous avez choisi.

--change-justification

Changer la justification du texte. Une valeur à "left" convertit tout le texte justifié dans la source vers un alignement de texte à gauche (c.à.d non justifié). Une valeur à "justify" convertit le texte non justifié en justifié. Une valeur à "original" (par défaut) ne change pas la justification du fichier source. Notez que seuls certains formats prennent en charge la justification.

--disable-font-rescaling

Désactiver tous les redimensionnements des tailles de polices.

--embed-all-fonts

Incorporer toutes les polices qui sont référencées dans le document mais pas encore intégrées. Une recherche sera effectuée sur votre ordinateur et si les polices sont retrouvées, elles seront intégrées dans le document. L'intégration fonctionnera uniquement si le format vers lequel vous convertissez supporte les polices intégrées, telles que EPUB, AZW3, DOCX et PDF. Veuillez vous assurer que vous possédez la licence appropriée pour incorporer les polices utilisées dans ce document.

--embed-font-family

Inclut la famille de polices spécifiée dans le livre. Ceci précise la police de "base" utilisée pour le livre. Si le document d'entrée spécifie ses propres polices, elles peuvent primer sur la police de "base". Vous pouvez utilisez l'option de filtre de style pour enlever les polices du document d'entrée. Notez que l'inclusion de polices fonctionne seulement avec certains formats de sortie, principalement l'EPUB,AZW3 et DOCX.

--expand-css

Par défaut, calibre va utiliser la forme abrégée des diverses propriétés CSS comme les marges, les bordures, etc. Cette option permet d'utiliser la forme étendue à la place. Notez que le CSS sera toujours sous sa forme étendue lors de la génération de fichiers EPUB avec le profil de sortie définit pour Nook car le Nook ne sait pas gérer la forme abrégée du CSS.

--extra-css

Soit le chemin d’une feuille de style CSS ou un CSS brut. Ce CSS sera ajouté aux règles de style du fichier source, ainsi il pourra être utilisé pour outrepasser ces règles.

--filter-css

Une liste de propriétés CSS, séparées par des virgules, qui seront supprimées de toutes les règles de style CSS. Ceci est utile si la présence de certaines informations de style les empêche d’être ignorées par votre périphérique. Par exemple: font-family,color,margin-left,margin-right

--font-size-mapping

Correspondance à partir de noms de police CSS vers des tailles de polices en pts. Un exemple de paramétrage est : 12,12,14,16,18,20,22,24. Ce sont les correspondances pour les tailles de xx-small à xx-large, la taille finale est utilisée pour les très grandes polices. Le redimensionnement de la police utilise ces tailles pour redimensionner intelligemment les polices. Le comportement par défaut est d’utiliser une correspondance basée sur le profil de sortie que vous avez choisi.

--insert-blank-line

Insérer une ligne blanche entre les paragraphes. Ne fonctionnera pas si le fichier source n’utilise pas de paragraphes. (balises <p> ou <div>)

--insert-blank-line-size

Définir la hauteur des lignes blanches insérées (en em). La hauteur des lignes entre deux paragraphes sera de deux fois la valeur spécifiée ici.

--keep-ligatures

Conserver les ligatures présentes dans le document d’entrée. Une ligature est d’une paire de caractères comme ff, fi, fl, œ, æ et cætera. La plupart des polices par défaut des lecteurs ne prennent pas en charge les ligatures, aussi un rendu correct de celles-ci semble improbable sur le lecteur. Par défaut, calibre va transformer une ligature en la paire de caractères correspondants. A l’opposé, cette option va conserver la ligature.

--line-height

La hauteur de ligne en pts. Contrôle l’espacement entre les lignes consécutives du texte. Ne s’applique qu’aux éléments qui ne définissent pas leur propre hauteur de ligne. Dans la plupart des cas, l’option hauteur de ligne minimum est plus utile. Par défaut, aucune modification de hauteur de ligne n’est effectuée.

--linearize-tables

Certains documents mal conçus utilisent les tableaux pour contrôler l’affichage du texte dans la page. Quand ils sont convertis, ces documents ont souvent le texte qui déborde de la page et d’autres dysfonctionnements. Cette option extraira le contenu des tableaux et le présentera dans un mode linéaire.

--margin-bottom

Régler la marge du bas en pts. Le réglage par défaut est 5.0. Aucune marge ne sera définie si le nombre est inférieur à zéro (le réglage des marges dans le document d'origine est préservé). Notez : les formats de page orientés tels que PDF et DOCX ont leurs propres paramétrages de marges qui ont la priorité.

--margin-left

Régler la marge gauche en pts. Le réglage par défaut est 5.0. Aucune marge ne sera définie si le nombre est inférieur à zéro (le réglage des marges dans le document d'origine est préservé). Notez : les formats de page orientés tels que PDF et DOCX ont leurs propres paramétrages de marge qui ont la priorité.

--margin-right

Régler la marge droite en pts. Le réglage par défaut est 5.0. Aucune marge ne sera définie si le nombre est inférieur à zéro (le réglage des marges dans le document d'origine est préservé). Notez : les formats de page orientés tels que PDF et DOCX ont leurs propres paramétrages de marge qui ont la priorité.

--margin-top

Régler la marge haute en pts. Le réglage par défaut est 5.0. Aucune marge ne sera définie si le nombre est inférieur à zéro (le réglage des marges dans le document d'origine est préservé). Notez : les formats de page orientés tels que PDF et DOCX ont leurs propres paramétrages de marge qui ont la priorité.

--minimum-line-height

La hauteur de ligne minimum, en pourcentage de la taille de la police de l’élément calculé. calibre s’assurera que chaque élément a une hauteur de ligne correspondant au moins à ce réglage, quelle que soit la spécification du document d’entrée. Fixer à 0 pour désactiver. Par défaut 120%. Utiliser cette option préférentiellement à une spécification de ligne directe, à moins de savoir ce que vous faites. Par exemple, vous pouvez obtenir un texte en "double interligne" en fixant cette valeur à 240%.

--remove-paragraph-spacing

Supprimer l’interligne entre les paragraphes. Paramètre aussi une indentation sur les paragraphes de 1.5em. La suppression de l’interligne ne fonctionnera pas si le fichier source n’utilise pas les paragraphes (balises <p> ou <div>).

--remove-paragraph-spacing-indent-size

Lorsque calibre supprime les lignes blanches entre les paragraphes, il ajoute automatiquement une indentation de paragraphe, afin de s’assurer que les paragraphes puissent être facilement distingués. Cette option contrôle la largeur de cette indentation (en em). Si vous fixez une valeur négative, alors l’indentation spécifiée dans le document d’entrée est utilisée, c’est à dire que calibre ne modifie pas l’indentation.

--smarten-punctuation

Convertit les guillemets, les tirets et les points de suspension en leurs équivalents typographiques corrects. Pour plus de détails, voir https://daringfireball.net/projects/smartypants.

--subset-embedded-fonts

Créer un sous-ensemble de toutes les polices intégrées. Chaque police intégrée est réduite pour ne contenir que les caractères utilisés dans le document. Cela réduit la taille des fichiers associés aux polices. Utile si vous souhaitez intégrer des polices particulièrement grandes dont peu de glyphes sont utilisés.

--transform-css-rules

Chemin vers un fichier contenant les règles pour transformer les styles CSS dans ce livre. La manière la plus facile de créer un tel fichier est d'utiliser l'assistant pour la création de règles dans l'interface graphique utilisateur de calibre. Accédez-y dans la section de la boite de dialogue conversion sous "Apparence->Transformer les styles". Une fois fois que vous avez créé les règles, vous pouvez utiliser le bouton Exporter pour les sauvegarder en un fichier.

--transform-html-rules

Chemin vers un fichier contenant les règles pour transformer du HTML dans ce livre. La manière la plus facile de créer un tel fichier est d'utiliser l'assistant pour la création de règles dans l'interface graphique utilisateur de calibre. Accédez-y dans la section de la boite de dialogue conversion sous "Apparence->Transformer du HTML". Une fois fois que vous avez créé les règles, vous pouvez utiliser le bouton "Exporter" pour les sauvegarder en un fichier.

--unsmarten-punctuation

Convertir les guillemets fantaisie, les tirets et les points de suspension en leurs équivalents en texte brut.

TRAITEMENT HEURISTIQUE

Modifier le texte et la structure du document selon le modèle courant. Désactivé par défaut. Utiliser –enable-heuristics pour l’activer. Les actions individuelles peuvent être désactivées avec les options –disable-*.

--disable-dehyphenate

Analyser les mots contenant des traits d'union dans le document. Le document lui-même est utilisé comme dictionnaire pour déterminer si les traits d'union doivent être conservés ou retirés.

--disable-delete-blank-paragraphs

Supprimer les paragraphes vides du document quand ils se trouvent entre d'autres paragraphes

--disable-fix-indents

Transformer les retraits de ligne créées par de multiples espaces insécables en retraits CSS.

--disable-format-scene-breaks

Les marqueurs de rupture de scène alignés à gauche sont alignés au centre. Les ruptures de scène qui utilisent des lignes blanches multiples sont remplacées par des traits horizontaux.

--disable-italicize-common-cases

Rechercher les mots et les motifs usuels qui dénotent la présence de mots en italiques et les mettre en italique.

--disable-markup-chapter-headings

Détecter les titre de chapitre et les sous-titres sans mise en forme et les transforme en balises h2 et h3. Ce réglage ne va pas créer de Table des Matières, mais peut être utilisé dans la partie Détection de structure afin d'en créer une.

--disable-renumber-headings

Recherche les occurrences de séquences de balises <h1> ou <h2>. Les balises sont renumérotées afin de d'éviter que les titres de chapitres ne soient coupés en deux.

--disable-unwrap-lines

Redéfinir la longueur des lignes en se basant sur la ponctuation et autres indices de formatage.

--enable-heuristics

Activer le traitement heuristique. Cette option doit être activée pour que tout traitement heuristique puisse être effectué.

--html-unwrap-factor

Échelle utilisée pour déterminer la longueur à partir de laquelle une ligne doit être redéfinie. Les valeurs acceptables sont les décimaux compris entre 0 et 1. La valeur pas défaut est de 0.4, juste en dessous de la valeur médiane des longueurs de ligne dans le document. Si seules quelques lignes dans le document nécessitent d’être redéfinies, cette valeur devrait être réduite.

--replace-scene-breaks

Remplacer les ruptures de scène par le texte spécifié. Par défaut, le texte du document initial est utilisé.

RECHERCHER ET REMPLACER

Modifier le texte et la structure du document en utilisant des modèles définis par l’utilisateur.

--search-replace

Chemin vers un fichier contenant les expressions régulières de recherche et remplacement. Le fichier doit contenir en alternances des lignes d'expressions régulières suivies par des modèles de remplacement (qui peuvent être une ligne vide). L'expression régulière doit être écrite dans la syntaxe des regexp Python et le fichier doit être encodé en UTF-8.

--sr1-replace

Le remplacement pour remplacer le texte trouvé avec sr1-search.

--sr1-search

Motif de recherche (expression régulière) qui sera remplacé par sr1-replace.

--sr2-replace

Le remplacement pour remplacer le texte a trouvé avec sr2-search.

--sr2-search

Modèle de recherche (expression régulière) qui sera remplacé par sr2-replace.

--sr3-replace

Le remplacement pour remplacer le texte a trouvé avec sr3-search.

--sr3-search

Modèle de recherche (expression régulière) qui sera remplacée par sr3-replace.

DÉTECTION DE LA STRUCTURE

Contrôle de l’auto-détection de la structure du document.

--chapter

Une expression XPath pour détecter les titres de chapitres. Par défaut, les étiquettes <h1> ou <h2> qui contiennent les mots "chapter", "book", "section", "prologue", "epilogue", ou "part" ainsi que les étiquettes qui contiennent class="chapter". L’expression utilisée doit être évaluée comme une liste d’éléments. Pour désactiver la détection de chapitre, utiliser l’expression "/". Regarder le tutoriel XPath dans le Manuel Utilisateur de calibre pour plus d’aide sur l’utilisation de cette fonction.

--chapter-mark

Spécifier comment indiquer les chapitres détectés. Une valeur "pagebreak" insérera un saut de page avant chaque chapitre. Une valeur "rule" insérera un filet avant chaque chapitre. Une valeur "none" désactivera le marquage des chapitres et une valeur "both" utilisera à la fois un saut de page et un filet.

--disable-remove-fake-margins

Certains documents spécifient les marges de la page en précisant une marge gauche et droite sur chaque paragraphe individuel. calibre tentera de détecter et de supprimer ces marges. Cela peut éventuellement provoquer la suppression de certaines marges qui ne devaient pas être retirées. Dans ce cas, vous pouvez désactiver la suppression de ces marges.

--insert-metadata

Insérer les métadonnées au début du livre. Utile si votre lecteur de livre numérique ne prend pas en charge directement l’affichage/recherche des métadonnées.

--page-breaks-before

Une expression XPath. Des sauts de page sont insérés avant les éléments spécifiés. Pour désactiver utiliser l'expression : /

--prefer-metadata-cover

Utiliser la couverture détectée dans le fichier source de préférence à la couverture spécifiée.

--remove-first-image

Supprimer la première image du livre numérique en entrée. Utile si le document à transformer a une image de couverture qui n’est pas identifiée comme une couverture. Dans ce cas, si vous avez une image de couverture dans calibre, le document de sortie aura deux images de couverture si vous n’utilisez pas cette option.

--start-reading-at

Expression XPath pour détecter l'emplacement du début de la lecture dans le document. Certains programmes de lecture de livre numérique (dont le plus connu est le Kindle) utilisent cette position comme emplacement d'ouverture du livre. Voir le Tutoriel XPath dans le Manuel d’utilisation de calibre pour une aide supplémentaire concernant cette fonctionnalité.

TABLE DES MATIERES

Contrôle la génération automatique de la Table des Matières. Par défaut, si le fichier source a une Table des Matières, elle sera utilisée de préférence par rapport à celle qui est générée automatiquement.

--duplicate-links-in-toc

Lors de la création d’une Table des Matières à partir de liens dans le document d’entrée, autorise des entrées dupliquées, à savoir autorise plus d’une entrée avec le même texte, du moment qu’elles pointent vers des emplacements différents.

--level1-toc

Expression XPath indiquant toutes les étiquettes qui doivent être ajoutées au premier niveau de la Table des Matières. Si elle est spécifiée, elle sera prioritaire par rapport aux autres formes d’auto-détection. Voir le Tutoriel XPath dans le Manuel Utilisateur calibre pour des exemples.

--level2-toc

Expression XPath indiquant que toutes les étiquettes doivent être ajoutées au deuxième niveau de la table des matières. Chaque entrée est ajoutée en dessous de la précédente entrée de premier niveau. Voir le Tutoriel XPath dans le Manuel Utilisateur calibre pour des exemples.

--level3-toc

Expression XPath indiquant que toutes les étiquettes doivent être ajoutées au troisième niveau de la table des matières. Chaque entrée est ajoutée en dessous de la précédente entrée de second niveau. Voir le Tutoriel XPath dans le Manuel Utilisateur calibre pour des exemples.

--max-toc-links

Nombre maximum de liens à insérer dans la table des matières. Régler à 0 pour désactiver. Par défaut : 50. Les liens sont ajoutés à la Table des Matières seulement si le seuil du nombre de chapitres détectés n’a pas été atteint.

--no-chapters-in-toc

Ne pas ajouter à la Table des Matières les chapitres détectés automatiquement.

--toc-filter

Supprimer les entrées de la Table des Matières qui correspondent à l’expression régulière spécifiée. Les entrées correspondantes ainsi que leurs sous-entrées seront supprimées.

--toc-threshold

Lorsque le nombre de chapitres détectés est inférieur à ce chiffre, les liens sont ajoutés à la Table des Matières. Par défaut : 6

--use-auto-toc

Normalement, si le fichier source a déjà une Table des Matières, elle est utilisée de préférence à celle auto-générée. Avec cette option, l’auto-générée est toujours utilisée.

METADONNEES

Options pour indiquer les métadonnées en sortie

--author-sort

Chaîne à utiliser lors du tri par auteur.

--authors

Indiquer les auteurs. Les auteurs multiples doivent être séparés par des & (esperluètes).

--book-producer

Déterminer le producteur du livre.

--comments

Indiquer la description du livre numérique.

--cover

Déterminer la couverture à partir du fichier spécifié ou de l’URL

--isbn

Déterminer l’ISBN du livre.

--language

Déterminer la langue.

--pubdate

Défini la date de publication (supposée être dans le fuseau horaire local, sinon le fuseau horaire est explicitement spécifié)

--publisher

Indiquer l’éditeur du livre numérique.

--rating

Déterminer la note. Devrait être un nombre entre 1 et 5.

--read-metadata-from-opf, --from-opf, -m

Lire les métadonnées du fichier OPF spécifié. Les métadonnées lues à partir de ce fichier écraseront les métadonnées dans le fichier source.

--series

Indiquer la série à laquelle appartient ce livre numérique.

--series-index

Déterminer l’index de ce livre numérique dans cette série.

--tags

Déterminer les étiquettes du livre. Doit être une liste séparée par des virgules.

--timestamp

Définir l’horodatage (plus utilisé nulle part)

--title

Indiquer le titre.

--title-sort

La version du titre à utiliser pour le tri.

DÉBOGAGE

Options pour aider au débogage de la conversion

--debug-pipeline, -d

Enregistre le résultat des différents cycles du processus de conversion dans le dossier spécifié. Utile si vous ne savez pas à quel moment du cycle de conversion le bug est généré.

--verbose, -v

Niveau de verbosité. Spécifier des périodes multiples pour une plus grande verbosité. La spécification par deux fois aura comme conséquence la pleine verbosité, la verbosité par une fois la moyenne et les périodes zéro moins de verbosité.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- ebook-edit

ebook-edit

ebook-edit [opts] [path_to_ebook] [name_of_file_inside_book ...]

Lance l’outil Edition de livre calibre. Vous pouvez optionnellement spécifier également les noms des fichiers dans le livre qui devront être ouverts automatiquement pour l’édition.

Chaque fois que vous passez à ebook-edit des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--detach

Détacher du terminal de contrôle, s'il y en a un (linux uniquement)

--help, -h

affiche ce message d'aide et quitte

--select-text

Le texte à sélectionner dans le livre lorsqu'il est ouvert pour être édité

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- ebook-meta

ebook-meta

ebook-meta ebook_file [options]

Lire/Écrire des métadonnées de/vers des fichiers de livre numérique.

Formats pris en charge pour la lecture des métadonnées: 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

Formats pris en charge pour l’écriture des métadonnées: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

Différents types de fichiers prennent en charge différentes sortes de métadonnées. Si vous essayer de définir certaines métadonnées sur un type de fichier qui ne le prend pas en charge, les métadonnées seront ignorées sans avertissement.

Chaque fois que vous passez à ebook-meta des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--author-sort

Chaine utilisée pour le tri par auteur. Si non spécifiée, et que le(s) auteur(s) sont spécifié(s), il sera généré automatiquement à partir de(s) (l')auteur(s).

--authors, -a

Définir les auteurs. Les auteurs multiples doivent être séparés par le caractère &. Les noms d’auteur devrait être dans l’ordre Prénom Nom.

--book-producer, -k

Déterminer le producteur du livre.

--category

Définir la catégorie du livre.

--comments, -c

Indiquer la description du livre numérique.

--cover

Définir la couverture pour le fichier spécifié.

--date, -d

Définir la date de publication.

--from-opf

Lire les métadonnées à partir du fichier OPF spécifié et les utiliser pour indiquer les méta-données du livre numérique. Les métadonnées spécifiées en ligne de commande écraseront les métadonnées lues à partir du fichier OPF.

--get-cover

Obtenir la couverture du livre numérique et l’enregistrer sous le fichier spécifié.

--help, -h

affiche ce message d'aide et quitte

--identifier

Définir les identifiants du livre, peut être spécifié à plusieurs reprises. Par exemple: --identifier uri:https://acme.com --identifier isbn:12345. Pour supprimer un identifiant, laissez la valeur en blanc, --identifier isbn:. Notez que pour les fichiers EPUB, un identifiant marqué comme identifiant de paquet ne peut pas être supprimé.

--index, -i

Déterminer l’index de ce livre numérique dans cette série.

--isbn

Déterminer l’ISBN du livre.

--language, -l

Déterminer la langue.

--lrf-bookid

Définir l’identifiant du livre dans les fichiers LRF

--publisher, -p

Indiquer l’éditeur du livre numérique.

--rating, -r

Déterminer la note. Devrait être un nombre entre 1 et 5.

--series, -s

Indiquer la série à laquelle appartient ce livre numérique.

--tags

Déterminer les étiquettes du livre. Doit être une liste séparée par des virgules.

--title, -t

Indiquer le titre.

--title-sort

La version du titre à utilisé pour le tri. Si non spécifiée, et que le titre est spécifié, il sera généré automatiquement à partir du titre.

--to-opf

Spécifier le nom du fichier OPF. Les métadonnées seront écrites dans le fichier OPF.

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- ebook-polish

ebook-polish

ebook-polish [options] input_file [output_file]

Le Polissage des livres constitue la dernière touche de perfection pour finaliser les livres numériques que vous avez soigneusement élaborés.

Le polissage tente de minimiser les modifications apportées au code interne de votre livre numérique.
Contrairement à la conversion, il n’aplatit pas les CSS, ne renomme pas les fichiers, ne change pas les tailles de police, n’ajuste pas les marges, etc…
Chaque action exécute seulement le minimum de modifications nécessaires pour obtenir l’effet désiré.

Vous pouvez utiliser cet outil en dernière étape de votre processus de création de livre numérique.

Notez que le polissage ne fonctionne que sur les fichiers aux formats AZW3 ou EPUB.

Chaque fois que vous passez à ebook-polish des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--add-soft-hyphens, -H

Ajouter des traits d'union conditionnels à tous les mots du livre. Cela permet au livre d'être mieux affiché lorsque le texte est justifié, pour les lecteurs ne supportant pas la césure.

--compress-images, -i

Compresser les images sans perte dans le livre, pour les réduire, sans affecter la qualité de l'image.

--cover, -c

Chemin vers une image de couverture. Change la couverture spécifiée dans le livre numérique. Si aucune couverture n'est présente ou si la couverture n'est pas identifiée correctement, insère une nouvelle couverture.

--embed-fonts, -e

Incorporer toutes les polices qui sont référencées dans le document mais pas encore intégrées. Une recherche sera effectuée sur votre ordinateur et si les polices sont trouvées, elles seront intégrées dans le document. Veuillez vous assurer que vous possédez la licence appropriée avant d'incorporer les polices utilisées dans ce document.

--help, -h

affiche ce message d'aide et quitte

--jacket, -j

Insérer une page "jaquette de livre" au début du livre, sur laquelle on peut lire les éléments de métadonnées tels que le titre, les étiquettes, les auteurs, les séries, les commentaires, etc… Toute jaquette du livre préexistante sera remplacée.

--opf, -o

Chemin vers un fichier OPF. Les métadonnées du livre sont mises à jour à partir du ficher OPF.

--remove-jacket

Enlever la page jaquette préexistante.

--remove-soft-hyphens

Supprime les traits d'union conditionnels de la totalité du texte du livre.

--remove-unused-css, -u

Supprimer toutes les règles CSS inutilisées des feuilles de style et des mots-clés <style> . Certains livres créés à partir de modèles de production peuvent comporter un grand nombre de règles CSS supplémentaires qui ne correspondent à aucun contenu présent. Ces règles supplémentaires peuvent ralentir certains lecteurs qui ont besoin de toutes les analyser.

--smarten-punctuation, -p

Convertir les tirets de texte brut, ellipses, guillemets, multiples traits d'union, etc… dans leurs équivalents typographiques corrects. Notez que l'algorithme peut parfois générer des résultats erronés, en particulier en présence d'apostrophes utilisées pour les contractions de mots.

--subset-fonts, -f

Créer un sous-ensemble de polices signifie réduire une police embarquée pour contenir uniquement les caractères utilisés de cette police dans le livre. Cela réduit considérablement la taille des fichiers de police (une réduction de moitié de la taille du fichier de police est fréquent). Par exemple, si le livre utilise une police spécifique pour les en-têtes, alors créer un sous-ensemble réduira la police pour ne contenir que les caractères réellement présents dans les en-têtes du livre. Ou si le livre intègre les versions en gras et italique d'une police, mais que le texte en italique ou en gras est relativement rare, voire inexistant, alors les caractères gras et en italiques peuvent être soit réduits à seulement quelques caractères, voire complètement retirés. Le seul inconvénient des sous-ensembles de polices, c'est que si, à une date ultérieure, vous décidez d'ajouter du texte à vos livres, le texte nouvellement ajouté pourrait ne pas être pris en compte par la police réduite.

--upgrade-book, -U

Met à jour la structure interne du livre, si possible. Par exemple, mise à jour de livres EPUB 2 en livres EPUB 3.

--verbose

Générer une sortie plus détaillée (utile pour le débogage)

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- ebook-viewer

ebook-viewer

ebook-viewer [options] fichier

Visualiser un livre numérique.

Chaque fois que vous passez à ebook-viewer des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--continue

Continuer à lire le dernier livre ouvert

--detach

Détacher du terminal de contrôle, s'il y en a un (linux uniquement)

--force-reload

Forcer le rechargement de tous les livres ouverts

--full-screen, --fullscreen, -f

Si précisé, la fenêtre de la visionneuse de livre numérique essaiera, au démarrage, de s’ouvrir en plein écran.

--help, -h

affiche ce message d'aide et quitte

--new-instance

Ouvrir une nouvelle fenêtre de visionneuse même si l'option d'utilisation d'une seule fenêtre de visionneuse est activée

--open-at

La position à laquelle ouvrir le livre spécifié. La position est un emplacement ou une position que vous pouvez obtenir en utilisant l'action Aller à->Location dans les contrôles de la visionneuse. Vous pouvez également utiliser le formulaire toc:quelque chose pour ouvrir le livre à l'emplacement de la première entrée de la table des matières qui contient la chaîne "quelque chose". Le formulaire toc-href:quelque chose correspondra au href (destination du lien interne) des nœuds toc. La correspondance est exacte. Si vous souhaitez faire correspondre une sous-chaîne, utilisez la forme toc-href-contains:quelque chose. Le formulaire ref:quelque chose utilisera Reference en mode référence. Le formulaire search:quelque chose recherchera quelque chose après avoir ouvert le livre. La forme regex:quelque chose recherchera l'expression régulière quelque chose après avoir ouvert le livre.

--raise-window

Si spécifié, la fenêtre de la visionneuse de livre numérique essaiera, au lancement, d’apparaître au premier plan.

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [options]

Récupérer des métadonnées de livre depuis des sources en ligne. Vous devez spécifier au moins le titre, les auteurs ou l’ISBN.

Chaque fois que vous passez à fetch-ebook-metadata des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--allowed-plugin, -p

Spécifier le nom d'une extension de téléchargement des métadonnées à utiliser. Par défaut, toutes les extensions de métadonnées seront utilisées. Peut être spécifié de multiples fois et pour de multiples extensions. Tous les noms d'extensions : Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search

--authors, -a

Auteur(s) du livre

--cover, -c

Spécifier un nom de fichier. La couverture, si disponible, y sera sauvée. Sans cette option, aucune couverture ne sera téléchargée.

--help, -h

affiche ce message d'aide et quitte

--identifier, -I

Les identificateurs tels que ASIN/Goodreads, etc. Peuvent être spécifiés plusieurs fois pour plusieurs identificateurs. Par exemple : --identifier asin:B0082BAJA0

--isbn, -i

ISBN du livre

--opf, -o

Produit les métadonnées dans le format d'OPF au lieu d'un texte lisible pour l'homme.

--timeout, -d

Délai d'attente en secondes. Par défaut est à 30

--title, -t

Titre du livre

--verbose, -v

Imprimer le journal vers la console (stderr)

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- lrf2lrs

lrf2lrs

lrf2lrs book.lrf

Convertir un fichier LRF en un fichier LRS (encodage XML UTF-8)

Chaque fois que vous passez à lrf2lrs des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--dont-output-resources

Ne pas enregistrer les images incorporées et les fichiers de polices sur le disque

--help, -h

affiche ce message d'aide et quitte

--output, -o

Fichier de sortie LRS

--verbose

Soyez plus détaillé

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- lrfviewer

lrfviewer

lrfviewer [options] book.lrf

Lit le livre numérique LRF livre.lrf

Chaque fois que vous passez à lrfviewer des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--disable-hyphenation

Désactive la césure. Devrait accélérer le rendu de manière significative.

--help, -h

affiche ce message d'aide et quitte

--profile

Profiler le rendu LRF

--verbose

Affiche plus d'information à propos du processus de rendu

--version

affiche le numéro de version du logiciel et quitte

--visual-debug

Active les aides visuelles pour corriger le moteur de rendu

--white-background

Par défaut le fond est blanc cassé que je trouve plus facile pour les yeux. Employez cette option pour faire un fond blanc pur.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- lrs2lrf

lrs2lrf

lrs2lrf [options] file.lrs

Compiler un fichier LRS en un fichier LRF.

Chaque fois que vous passez à lrs2lrf des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--help, -h

affiche ce message d'aide et quitte

--lrs

Convertir LRS en LRS, utile pour déboguer.

--output, -o

Chemin vers le fichier de sortie

--verbose

Traitement explicatif

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Interface en ligne de commande »
- web2disk

web2disk

web2disk URL

Où URL est par exemple https://google.com

Chaque fois que vous passez à web2disk des arguments comportant des espaces, entourez l’argument à l’aide de guillemets. Par exemple: « /some path/with spaces »

[options]

--base-dir, -d

Dossier de base dans lequel l’URL est sauvegardée. Par défaut : .

--delay

Intervalle minimal entre deux téléchargements, en secondes. 0 par défaut.

--dont-download-stylesheets

Ne pas télécharger les feuilles de style CSS.

--encoding

Encodage des caractères pour les sites web que vous essayer de télécharger. Par défaut, essaie de trouver l’encodage.

--filter-regexp

N’importe quel lien correspondant à cette expression régulière sera ignoré. Cette option peut-être activée plusieurs fois, auquel cas n’importe quel lien sera ignoré aussi longtemps qu’il correspond à une expression régulière. Par défaut, aucun lien n’est ignoré. Si l’expression régulière filtre et correspondance sont spécifiées, alors l’expression régulière filtre est appliquée en premier.

--help, -h

affiche ce message d'aide et quitte

--match-regexp

Seuls les liens correspondant à cette expression régulière seront suivis. Cette option peut être spécifiée plusieurs fois auquel cas tout lien qui correspondra à cette expression sera suivi. Par défaut, tous les liens sont suivis.

--max-files, -n

Nombre maximum de fichiers à télécharger. Ceci ne sera valable que pour les fichiers à partir des balises <a href>.Par défaut : 9223372036854775807

--max-recursions, -r

Nombre maximum de niveaux de récursivité c-à-d le niveau de profondeur des liens à suivre. Par défaut : 1

--timeout, -t

Délai d’attente d'une réponse du serveur, en secondes. Par défaut : 10.0 s

--verbose

Affiche des informations détaillées. Utile pour le débogage

--version

affiche le numéro de version du logiciel et quitte

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »

- Mise en place d’un environnement de développement calibre

Mise en place d’un environnement de développement calibre
calibre est complètement open source, sous licence GNU GPL v3 [https://www.gnu.org/copyleft/gpl.html]. Cela signifie que vous êtes libre de télécharger et de modifier le programme à votre convenance. Dans cette section, vous apprendrez comment obtenir la mise en place d’un environnement de développement calibre sur le système d’exploitation de votre choix. calibre est principalement écrit en Python [https://www.python.org] avec un peu de code C/C++ pour la vitesse et l’interfaçage système. Notez que calibre requiert au moins Python 3.8.

Contenu

- Philosophie de conception

- Disposition du code

- Obtenir le code

- Soumettre vos changements à inclure

- Environnement de développement Windows

- Environnement de développement macOS

- Environnement de développement Linux

- Avoir des installations séparées de calibre « normale » et « développement » sur le même ordinateur

- Astuces de dépannage

- En utilisant les déclarations d’impression

- Utilisation d’un interpréte Python interactif

- Utiliser le débogueur de Python comme débogueur à distance

- Utiliser le débogueur dans votre IDE Python préféré

- Exécuter des scripts quelconques dans l’environnement Python calibre

- Utilisation de calibre dans vos projets

- Installation binaire de calibre

- Installez les sources sur Linux

- Documentation API pour différentes parties de calibre

Philosophie de conception
calibre a ses racines dans le monde UNIX, ce qui signifie que sa conception est très modulaire. Les modules interagissent les uns avec les autres par l’intermédiaire d’interfaces bien définies. C’est ce qui rend l’ajout de nouvelles fonctionnalités et la réparation des dysfonctionnements très faciles dans calibre, ayant pour résultat un rythme de développement frénétique. En raison de ses racines,calibre a une interface en ligne de commande complète pour toutes ses fonctions, documentée dans doc:generated/fr/cli-index.

La conception modulaire de calibre est exprimée par l’intermédiaire des Extensions. Il y a un tutoriel sur l’écriture d’extensions calibre. Par exemple, ajouter le support d’un nouveau périphérique à calibre implique typiquement d’écrire moins de 100 lignes de code sous forme d’une extension pilote de périphérique. Vous pouvez parcourir les pilotes intégrés [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices]. Similairement, ajouter le support pour de nouveaux formats de conversion implique l’écriture de nouvelles extensions de format entrée/sortie. Un autre exemple de la conception modulaire est le système de recette pour récupérer des actualités. Pour plus d’exemples d’extensions conçues pour ajouter des fonctionnalités à calibre, voir l”Index des extensions [https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767].

Disposition du code

Tout le code Python de calibre est dans le paquet calibre. Ce paquet contient les principaux sous-paquets suivants :

- périphériques - Tous les pilotes de périphériques. Regardez juste certains des pilotes intégrés pour avoir une idée de la façon dont ils fonctionnent.

- Pour plus de détails, voir : devices.interface qui définit l’interface supportée par les pilotes de périphériques et devices.usbms qui définit un pilote générique qui connecte un périphérique USBMS. Tous les pilotes basés USBMS dans calibre héritent de lui.

- livres numériques - Tout le code conversion/métadonnées de livre numérique. Un bon point de départ est calibre.ebooks.conversion.cli qui est le module alimentant la commande ebook-convert. Le processus de conversion est contrôlé par l’intermédiaire de conversion.plumber. Le code de format indépendant est entièrement dans ebooks.oeb et le code de format dépendant est dans ebooks.format_name.

- La lecture, l’écriture et le téléchargement des métadonnées est entièrement dans ebooks.metadata

- La conversion se produit dans un pipeline, pour la structure du pipeline, voir Introduction. Le pipeline consiste en une extension d’entrée, différentes transformations et une extension de sortie. Le code qui construit et pilote le pipeline est dans plumber.py. Le pipeline fonctionne sur une représentation d’un livre numérique qui est comme un epub décompressé, avec le manifeste, la structure organisationnelle, la TdM, le guide, le contenu html, etc. La classe qui gère cette représentation est OEBBook dans ebooks.oeb.base. Les diverses transformations qui sont appliquées au livre durant la conversion se situent dans oeb/transforms/*.py. Et les extensions d’origines et de sortie se situent dans conversion/plugins/*.py.

- L’édition de livre numérique se produit en utilisant un objet conteneur différent. Il est documenté dans Documentation API pour l’outil d’édition de livre numérique.

- db - Le back-end de la base de données. Voir Documentation API pour l’interface base de données pour l’interface à la bibliothèque calibre.

- Serveur de contenu : srv est le Serveur de contenu calibre.

- gui2 - L’Interface Graphique Utilisateur. L’initialisation du GUI se produit dans gui2.main et gui2.ui. La visionneuse de livre numérique dans gui2.viewer. L’éditeur de livre numérique dans gui2.tweak_book.

Si vous voulez localiser les points d’entrée de tous les différents exécutables de calibre, regardez à la structure entry_points dans linux.py [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py].

Si vous avez besoin d’aide pour comprendre le code, postez sur le forum de développement [https://www.mobileread.com/forums/forumdisplay.php?f=240] et vous obtiendrez très probablement de l’aide de l’un des nombreux développeurs de calibre.

Obtenir le code

Vous pouvez obtenir le code source de calibre de deux manières soit en utilisant un système de contrôle de version soit en téléchargeant directement une tarball [https://calibre-ebook.com/dist/src].

calibre utilise Git [https://www.git-scm.com/], un système de contrôle de version distribuée. Git est disponible sur toutes les plateformes supportées par calibre. Après avoir installé Git, vous pouvez obtenir le code source de calibre avec la commande:

git clone https://github.com/kovidgoyal/calibre.git

Sous Windows, vous aurez besoin du nom de chemin complet, ce sera quelque chose comme C:\Program Files\Git\git.exe.

calibre est un très grand projet, avec une très longue histoire de contrôle de source, aussi ce qui est décrit plus haut peut prendre un moment (10 minutes à une heure dépendant de la vitesse de votre connexion internet).

Si vous voulez obtenir le code source plus rapidement, le code source de la dernière version est toujours disponible comme archive [https://calibre-ebook.com/dist/src].

Pour mettre à jour une branche du dernier code, utilisez la commande:

git pull --no-edit

Vous pouvez également parcourir le code sur GitHub [https://github.com/kovidgoyal/calibre].

Soumettre vos changements à inclure

Si vous planifiez de faire quelques petits changements, vous pouvez faire vos changements et crée une « directive de fusion » que vous pouvez alors attacher à un ticket dans le bug tracker [https://bugs.launchpad.net/calibre] de calibre. Pour faire cela, faites vos changements, et exécutez:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

Cela créera un fichier my-changes dans le dossier actuel, attachez le simplement à un ticket sur le bug tracker [https://bugs.launchpad.net/calibre] de calibre. Notez que ceci inclura toutes les validations effectuées. Si vous voulez seulement envoyer une partie des validations, vous devez changer origin/master ci-dessus. Pour envoyer seulement la dernière validation, employez:

git format-patch HEAD~1 --stdout > my-changes

Pour envoyer les dernières n validations, remplacer 1 avec n, par exemple, pour les 3 derniers validations:

git format-patch HEAD~3 --stdout > my-changes

Faites attention à ne pas inclure des fusions en employant HEAD~n.

Si vous planifiez de faire beaucoup de développement sur calibre, alors la meilleure méthode est de créer un compte GitHub [https://github.com]. Ci-dessous se trouve un guide basique pour mettre en place votre propre embranchement de calibre d’une manière qui vous permet de soumettre des requêtes de déchargement pour l’inclusion dans le dépôt principal calibre :

- Installer git sur votre machine comme décrit dans cet article : Installer Git [https://help.github.com/articles/set-up-git]

- Installez les clés SSH pour l’authentification sur GitHub, comme décrit ici : Generating SSH keys [https://help.github.com/articles/generating-ssh-keys]

- Allez sur https://github.com/kovidgoyal/calibre et cliquez sur le bouton Fork.

- Dans un Terminal faites:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

Remplacez <username> ci-dessus par votre nom d’utilisateur Github. Cela obtiendra que votre embranchement sera vérifié localement.

- Vous pouvez y faire des changements et des validations chaque fois que vous le désirez. Quand vous êtes prêt à fusionner votre travail, faites un:

git push

et rendez vous sur https://github.com/<username>/calibre et cliquez le bouton Pull Request pour générer une requête de déchargement qui peut être fusionnée.

- Vous pouvez mettre à jour votre copie locale du code depuis le dépôt principal à n’importe quel moment en faisant:

git pull upstream

Vous devriez aussi garder un oeil sur le forum de développement [https://www.mobileread.com/forums/forumdisplay.php?f=240] calibre. Avant de faire des changements majeurs, vous devriez en discuter dans le forum ou contacter directement Kovid (son adresse mail est partout dans le code source).

Environnement de développement Windows

Note

Vous devez aussi récupérer le code source calibre séparément comme décrit plus haut.

Installez calibre normalement, en utilisant l’installateur de Windows [https://calibre-ebook.com/download_windows]. Ouvrez alors une Invite de Commande et changez le dossier calibre précédemment vérifié. Par exemple:

cd C:\Users\kovid\work\calibre

calibre est le dossier qui contient les sous-dossiers src et resources.

L’étape suivante est de régler la variable d’environnement CALIBRE_DEVELOP_FROM au chemin absolu du dossier src. Ainsi, en suivant l’exemple ci-dessus, il devrait être C:\Users\kovid\work\calibre\src. Voici un court guide [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] pour paramétrer les variables d’environnement sous Windows.

Une vois que vous avez réglé la variable d’environnement, ouvrez une nouvelle invite de commande et vérifiez qu’elle est correctement paramétrée en utilisant la commande:

echo %CALIBRE_DEVELOP_FROM%

Paramétrer cette variable d’environnement signifie que calibre chargera maintenant tout son code Python à partir de l’emplacement spécifié.

Voilà. vous êtes maintenant prêt à commencer à bidouiller le code calibre. Par exemple, ouvrez le fichier src\calibre\__init__.py dans votre éditeur préféré et ajoutez la ligne:

print("Hello, world!")

près du dessus du ficher. Exécutez maintenant la commande calibredb. La toute première ligne de production devrait être Hello, world!.

Vous pouvez également mettre en place l’environnement de développement dans le Microsoft Visual Studion libre, si vous préférez, suivre les instructions ici [https://www.mobileread.com/forums/showthread.php?t=251201].

Environnement de développement macOS

Note

Vous devez aussi récupérer le code source calibre séparément comme décrit plus haut.

Installez calibre normalement en utilisant les .dmg mis à disposition [https://calibre-ebook.com/download_osx]. Ouvrez alors un Terminal et changez pour le dossier de code précédemment vérifié, par exemple:

cd /Users/kovid/work/calibre

calibre est le dossier qui contient les sous-dossiers src et resources. Les outils en ligne de commande calibre se trouvent à l’intérieur du paquet d’applications calibre, dans /Applications/calibre.app/Contents/MacOS vous devez ajouter ce dossier à votre variable d’environnement PATH, si vous voulez exécuter facilement les outils en ligne de commande.

L’étape suivante est de créer un script bash qui paramétrera les variables d’environnement CALIBRE_DEVELOP_FROM` au chemin absolu du dossier src lors de l’exécution de calibre en mode dépannage.

Créez un fichier texte brut:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

Sauvegardez le fichier comme /usr/bin/calibre-develop, paramétrer alors ses permissions afin qu’il puisse être exécuté

chmod +x /usr/local/bin/calibre-develop

Une fois que vous avez fait cela, exécuter :

calibre-develop

Vous devriez voir quelques informations diagnostiques dans la fenêtre Terminal au démarrage de calibre, et vous devriez voir une astérisque après le numéro de version dans la fenêtre GUI, indiquant que vous exécutez depuis la source.

Environnement de développement Linux

Note

Vous devez aussi récupérer le code source calibre séparément comme décrit plus haut.

calibre est principalement développé sous Linux. Vous avez deux choix pour paramétrer l’environnement de développement. Vous pouvez installer le binaire calibre comme à la normale et l’employer comme environnement d’exécution pour faire votre développement. Cette approche est similaire à celle utilisée dans Windows et macOS. Alternativement, vous pouvez installer calibre depuis la source. Les instructions pour installer un environnement de développement depuis la source sont dans le fichier INSTALL dans l’arborescence de source. Ici nous nous adresserons en utilisant le binaire d’exécution, qui est la méthode recommandée.

Installer calibre en utilisant l’installateur binaire [https://calibre-ebook.com/download_linux]. Ouvrez alors un terminal et changez le dossier du code calibre précédemment vérifié, par exemple:

cd /home/kovid/work/calibre

calibre est le dossier qui contient les sous-dossiers src et resources.

L’étape suivante est de créer un script bash qui paramétrera les variables d’environnement CALIBRE_DEVELOP_FROM au chemin absolu du dossier src. Aussi, en suivant l’exemple ci-dessus, il devrait être /home/kovid/work/calibre/src. Comment paramétrer les variables d’environnement dépend de votre distribution Linux et de quel shell vous utilisez.

Note

Il est recommandé d’utiliser l’installeur binaire fourni en amont. Si vous insistez pour utiliser un paquet fourni par votre distribution, utilisez plutôt les variables CALIBRE_PYTHON_PATH et CALIBRE_RESOURCES_PATH.

Une fois que vous avez paramétré la variable d’environnement, ouvrez un nouveau terminal et vérifiez qu’elle a été correctement paramétrée en utilisant la commande:

echo $CALIBRE_DEVELOP_FROM

Paramétrer cette variable d’environnement signifie que calibre chargera maintenant tout son code Python à partir de l’emplacement spécifié.

Voilà ! Vous êtes maintenant prêt à commencer à bidouiller le code calibre. Par exemple, ouvrez le fichier src\calibre\__init__.py dans votre éditeur préféré et ajoutez la ligne:

print("Hello, world!")

près du dessus du ficher. Exécutez maintenant la commande calibredb. La toute première ligne de production devrait être Hello, world!.

Avoir des installations séparées de calibre « normale » et « développement » sur le même ordinateur

L’arborescence de source calibre est très stable et se rompt rarement, mais si vous sentez la nécessité d’exécuter depuis la source sur une bibliothèque distincte d’essai et de d’exécuter une version libre de calibre avec votre bibliothèque quotidienne, vous pouvez réaliser ceci facilement en utilisant des fichiers .bat ou des scripts shell pour lancer calibre. L’exemple ci-dessous expose comment faire ceci sur Windows utilisant des fichiers .bat (les instructions pour d’autres plates-formes sont identiques, employez juste un script shell au lieu d’un fichier .bat)

Pour lancer la nouvelle version de calibre avec votre bibliothèque quotidienne :

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"

Astuces de dépannage

Python est un langage opérant sur des objets de types déterminés dynamiquement avec d’excellents équipements pour l’introspection. Kovid a écrit le noyau du code calibre sans utiliser une fois un débogueur. Il y a beaucoup de stratégies pour corriger le code calibre :

En utilisant les déclarations d’impression

C’est la manière favorite de Kovid pour dépanner. Insérez simplement des déclarations d’impression aux points d’intérêt et exécuter votre programme dans le terminal. Par exemple : vous pouvez démarrer le GUI à partir du terminal comme:

calibre-debug -g

Similairement, vous pouvez démarrer la visionneuse de livre numérique comme

calibre-debug -w /path/to/file/to/be/viewed

The e-book editor can be started as:

calibre-debug --edit-book /path/to/be/edited

Utilisation d’un interpréte Python interactif

Vous pouvez insérer les deux lignes de code suivantes pour démarrer la session interactive Python à cet endroit:

from calibre import ipython
ipython(locals())

Lors de l’exécution à partir de la ligne de commande, cela démarrera l’interpréteur python interactif avec accès à toutes les variables définies localement (variables dans la portée locale). L’invite interactive a même une complétion Tab pour les propriétés d’objet et vous pouvez utiliser les diverses facilités Python pour l’introspection, telles que dir(), type(), repr(), etc.

Utiliser le débogueur de Python comme débogueur à distance

Vous pouvez utiliser le débogueur intégré de Python (pdb) comme débogueur à distance en ligne de commande. D’abord, démarrez le débogueur à distance au point qui vous intéresse dans le code de calibre, comme ceci:

from calibre.rpdb import set_trace
set_trace()

Exécuter alors calibre, soit comme normal, ou en utilisant une des commandes calibre-debug décrite dans la section précédente. Une fois le point ci-dessus atteint dans le code, calibre gèlera, attendant le débogueur pour se connecter.

Ouvrez maintenant un terminal ou une invite de commande et utilisez la commande suivante pour démarrer la session de débogage:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Vous pouvez lire à propos de comment utiliser le débogueur Python dans Python stdlib docs for the pdb module [https://docs.python.org/library/pdb.html#debugger-commands].

Note

Par défaut, le débogueur à distance essaiera de se connecter sur le port 4444. Vous pouvez le changer, en modifiant le paramètre de port des fonctions set_trace() et cli() ci-dessus, comme ceci : set_trace(port=1234) et cli(port=1234).

Note

Le débogueur de Python ne peut pas manipuler des fils multiples, aussi vous devez appeler le set_trace une fois par fil, chaque fois avec un numéro de port différent.

Utiliser le débogueur dans votre IDE Python préféré

Il est possible d’utiliser le débogueur intégré dans votre IDE Python préféré, s’il supporte le débogage à distance. La première étape est d’ajouter la src vérifiée calibre au PYTHONPATH dans votre IDE. En d’autres mots, le dossier que vous avec paramétré plus haut comme CALIBRE_DEVELOP_FROM, doit aussi être dans le PYTHONPATH de votre IDE.

Placez alors me module IDE de débogage à distance dans le sous-dossier src du code source vérifié calibre. Ajoutez ni’importe quel nécessaire pour lancer le débogueur à distance sur calibre au point d’intérêt, par exemple dans la fonction principale. Exécutez alors calibre comme normalement. Votre IDE devrait être capable de se connecter au débogueur à distance s’exécutant dans calibre.

Exécuter des scripts quelconques dans l’environnement Python calibre

La commande calibre-debug fournit quelques commutateurs pratiques pour exécuter votre propre code, avec l’accès aux modules calibre:

calibre-debug -c "some Python code"

est bien pour examiner un petit extrait de code sur la ligne de commande. Cela fonctionne de la même manière que le commutateur -c de l’interpréteur Python:

calibre-debug myscript.py

peut être employé pour exécuter votre propre script python. Cela fonctionne de la même manière que de passer le script à l’interpréteur python, sauf que l’environnement de calibre est entièrement initialisé, ainsi vous pouvez employer tout le code de calibre dans votre script. Pour employer les arguments en ligne de commande avec votre script, utilisez la forme:

calibre-debug myscript.py -- --option1 arg1

-- entraîne tous les arguments suivants d’être passés à votre script.

Utilisation de calibre dans vos projets

Il est possible d’utiliser d’utiliser les fonctions/code calibre dans votre projet Python. Deux manières existent pour faire cela :

Installation binaire de calibre

Si vous avez une installation binaire de calibre, vous pouvez utiliser l’interpréteur python empaqueté avec calibre, comme ceci:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Installez les sources sur Linux

En complément de l’utilisation de la technique ci-dessus, si vos faites une installation des sources sur Linux, vous pouvez aussi importer directement calibre, comme suit:

import init_calibre
import calibre

print(calibre.__version__)

Il est essentiel que vous importiez le module init_calibre avant tout autre modules/package calibre comme il a installé l’interpréteur pour exécuter le code calibre.

Documentation API pour différentes parties de calibre

- Documentation API pour les recettes

- Documentation API pour les extensions

- Documentation API pour l’interface base de données

- Documentation API pour l’outil d’édition de livre numérique

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »

- Mise en place d’un environnement de développement calibre »

- Documentation API pour l’interface base de données

Documentation API pour l’interface base de données

Cette API est un fil sûr (Elle utilise un lecteur multiple, un schéma rédacteur unique fermé). Vous pouvez accéder à cette API comme ceci :

from calibre.library import db
db = db('Path to calibre library folder').new_api

Si vous êtes dans une extension calibre qui est partie prenante du GUI principal de calibre, vous y obtenez l’accès comme ceci à la place :

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[source]

Un cache mémoire du fichier metadata.db depuis une bibliothèque calibre. Cette classe sert également comme une API de sécurité pour accéder à la base de données. Le cache mémoire est maintenu dans une forme normale pour une performance maximale.

SQLITE est simplement utilisée comme une manière robuste de lire et écrire depuis metadata.db. Toute table lecture/tri/recherche/cache est ré-implémentée. Ceci était nécessaire pour un maximum de performance et de flexibilité.

class EventType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

book_created = 4

Lorsqu’un nouvel enregistrement de livre est créé dans la base de données, avec comme seul argument l’identifiant du livre

book_edited = 8

Lorsqu’un format de livre est édité, avec des arguments : (book_id, fmt)

books_removed = 5

Lorsque des livres sont retirés de la base de données avec la liste des identifiants de livres comme seul argument

format_added = 2

Lorsqu’un format est ajouté à un livre, avec les arguments : (book_id, format)

formats_removed = 3

Lorsque des formats sont retirés d’un livre, avec des arguments : (correspondance entre l’identifiant du livre et l’ensemble des formats supprimés du livre)

indexing_progress_changed = 9

Lorsque la progression de l’indexation change

items_removed = 7

Lorsque des éléments tels que les étiquettes ou les auteurs sont supprimés de certains livres. Arguments : ( field_name, ids des livres concernés, ids des éléments supprimés)

items_renamed = 6

Lorsque des éléments tels que les étiquettes ou les auteurs sont renommés dans certains ou tous les livres. Arguments : ( field_name, ids de livres concernés, mappage de l’ancien id d’élément vers le nouvel id d’élément)

metadata_changed = 1

Lorsque certaines métadonnées sont modifiées pour certains livres, avec des arguments : (nom du champ modifié, ensemble d’identifiants de livres concernés)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[source]

Ajouter les livres spécifiés à la bibliothèque. Les livres doivent être un itérable de 2-tuples, chaque 2-tuple sous la forme (mi, format_map) où mi est un objet métadonnée et format_map est un dictionnaire de la forme {fmt: path_or_stream}, par exemple : {'EPUB': '/path/to/file.epub'}.

Renvoie une paire de listes : ids, duplicates. ids contient les ids de livre pour tous les livres nouvellement créés dans la base de données. duplicates contient (mi, format_map) pour tous les livres qui existent déjà dans la base de données selon l’heuristique simple de détection de doublons utilisée par has_book().

add_custom_book_data(name, val_map, delete_first=False)[source]

Ajouter les données pour le nom où val_map est une carte des book_ids vers les valeurs. Si delete_first est Vrai, toutes les données stockées précédemment seront supprimées.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[source]

Ajouter des fichiers de données supplémentaires

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[source]

Ajouter un format au livre spécifié. Renvoie True si le format a été ajouté avec succès.

Paramètres:

- replace – Si Vrai, remplace le format existant, autrement si le format existe déjà, renvoie Faux.

- run_hooks – Si Vrai, les extensions de type de fichier sont exécutées. dans le format avant et après avoir été ajoutées

- dbapi – Usage interne uniquement.

add_listener(event_callback_function, check_already_added=False)[source]

Enregistrer une fonction de rappel qui sera appelée après que certaines actions aient été effectuées sur cette base de données. La fonction doit prendre trois arguments : (EventType, library_id, event_type_specific_data)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[source]

Add the specified resource so it can be referenced by notes and return its content hash

all_book_ids(type=<class 'frozenset'>)[source]

Ensemble gelé de tous les ids de livre connus.

all_field_for(field, book_ids, default_value=None)[source]

Identique à field_for, excepté qu’il agit sur de multiples livres en une fois

all_field_ids(name)[source]

Ensemble gelé d’ids pour toutes les valeurs dans le champ``name``.

all_field_names(field)[source]

Ensemble gelé de tous les champs de noms (devrait uniquement être utilisé pour les champs many-one et many-many)

author_data(author_ids=None)[source]

Renvoie la donnée auteur comme un dictionnaire avec des clés : nom, genre, lien

Si aucuns auteurs avec les ids spécifiés ne sont trouvés un dictionnaire vide est renvoyé. Si author_ids est None, les données pour tous les auteurs sont renvoyées.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[source]

En fonction d’une liste d’auteurs, renvoie la chaîne author_sort pour les auteurs, en préférant l’auteur assorti associé à l’auteur plutôt que la chaîne calculée.

books_for_field(name, item_id)[source]

Renvoie tous les livres associés avec l’élément identifié par item_id, où l’élément appartient au champ name.

La valeur retournée est un ensemble d’ids de livre, ou l’ensemble vide si l’élément n’existe pas.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[source]

Affiche l’ensemble des livres dans la bibliothèque virtuelle spécifiée

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[source]

Compresser les images de couverture des livres spécifiés. Une qualité de compression de 100 effectuera une compression sans perte, sinon la compression sera avec perte.

Le rappel d’avancement sera appelé avec le book_id et les anciens et nouveaux formats pour chaque livre qui a été traité. Si une erreur se produit, la nouvelle taille sera une chaîne de caractères contenant les détails de l’erreur.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[source]

Copier la couverture dans le fichier comme un objet dest. Renvoie False si aucune couverture n’existe ou dest est le même fichier que la couverture actuelle. dest peut aussi être un chemin dans tel cas la couverture y est copiée si le chemin est différent du chemin actuel (prendre en compte la sensibilité à la casse).

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[source]

Copier le format fmt dans le fichier comme un objet dest. Si le format spécifié n’existe pas, indique l’erreur NoSuchFormat. dest peut aussi être un chemin (vers un fichier) dans tel cas la couverture y est copiée si et seulement si le chemin est différent du chemin actuel (prendre en compte la sensibilité à la casse).

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[source]

Renvoie l’image de couverture ou Aucun. Par défaut, renvoie la couverture comme une chaîne de caractères.

ATTENTION : l’utilisation de as_path copiera la couverture vers un fichier temporaire et renverra le chemin vers ce fichier temporaire. Vous devez supprimer le fichier temporaire quand vous en avez terminé avec celui-ci.

Paramètres:

- as_file – Si Vrai renvoie l’image en tant qu’objet fichier ouvert (une file d’attente de fichiers temporaires)

- as_image – Si vrai renvoie l’image comme un objet QImage

- as_pixmap – Si True renvoie l’image comme un objet QPixmage

- as_path – Si vrai renvoie l’image comme un chemin pointant vers un fichier temporaire

data_for_find_identical_books()[source]

Renvoie les données qui peuvent être utilisées pour implémenter find_identical_books() dans un processus de travail sans accès à la bd. Voir db.utils pour une implémentation.

data_for_has_book()[source]

Renvoie les données appropriées pour être utilisées dans has_book(). Ceci peut être utilisé pour une implémentation de has_book() dans un processus de travail sans accès à la bd.

delete_custom_book_data(name, book_ids=())[source]

Effacer les données pour le nom. Par défaut efface toutes les données, si vous voulez seulement effacer les données pour certaines ids de livres, passez dans une liste d’ids de livres.

delete_trash_entry(book_id, category)[source]

Supprimer une entrée de la corbeille. Ici, la catégorie est “b” pour les livres et “f” pour les formats.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[source]

Mettre à jour les métadonnées pour tous les fomats du book_ids sélectionné vers les métadonnées actuelle dans la base de données.

expire_old_trash()[source]

Expirer les entrées trop anciennes de la corbeille

export_note(field, item_id) → str[source]

Export the note as a single HTML document with embedded images as data: URLs

fast_field_for(field_obj, book_id, default_value=None)[source]

Semblable à field_for, sauf qu’il évite la consultation supplémentaire pour obtenir l’objet du champ

field_for(name, book_id, default_value=None)[source]

Renvoie la valeur du champ name pour le livre identifié par book_id. Si un tel livre n’existe pas ou s’il n’y a pas de valeur spécifiée pour le champ name` ou qu'un tel champ n'existe pas, alors ``default_value est renvoyée.

default_value n’est pas utilisée pour title, title_sort, authors, author_sort and series_index. C’est parce que ceux-ci ont toujours des valeurs dans la base de données. default_value est utilisée pour toutes les colonnes personnalisées.

La valeur renvoyée pour les champs is_multiple est toujours un tuples, même si aucune valeur n’est trouvée (en d’autres termes, default_value est ignoré). L’exception concerne les identificateurs, pour lesquels la valeur retournée est toujours un dictionnaire. Les tuples renvoyés sont toujours dans l’ordre des liens, c’est-à-dire l’ordre dans lequel ils ont été créés.

field_ids_for(name, book_id)[source]

Renvoie les ids (en tant que tuples) pour les valeurs que le champ name à dans le livre identifié par book_id. S’il n’y a pas de valeurs, ou pas de tel livre, ou pas de tel champ, un tuple vide est renvoyé.

field_supports_notes(field=None) → bool[source]

Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes.

find_identical_books(mi, search_restriction='', book_ids=None)[source]

Trouve les livres qui ont un sur-ensemble d’auteurs dans mi et ont le même titre (titre est à correspondance floue). Voir aussi data_for_find_identical_books().

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[source]

Renvoie le format de livre numérique comme une chaîne de caractères ou None si le format n’existe pas, ou que nous n’avons pas la permission d’écrire dans le fichier de livre numérique.

Paramètres:

- as_file – Si True le format de livre numérique est renvoyé comme un objet fichier. Notez que l’objet fichier est un fichier temporaire de file d’attente, aussi si ce que vous voulez faire est de copier le format sur un autre fichier, utilisez copy_format_to() à la place pour la performance.

- as_path – Copie le format de fichier vers un fichier temporaire et renvoie le chemin vers le fichier temporaire

- preserve_filename – Si Vrai et renvoie un chemin le nom de fichier est le même que celui utilisé dans la bibliothèque. Notez qu’utiliser ceci signifie donner des appels répétés du même fichier temporaire (qui est recréé chaque fois)

format_abspath(book_id, fmt)[source]

Renvoie le chemin absolu du fichier livre numérique du format format. Vous ne devriez presque jamais utiliser ceci, car compromet la promesse threadsafe de cette API. A la place utilisez, copy_format_to().

Actuellement utilisé uniquement dans la liste calibredb, la visionneuse, éditer le livre, compare_format au format original, Open With et les catalogues (via get_data_as_dict()).

A part depuis la visionneuse, open with et éditer le livre, je ne crois pas qu’aucun des autres ne fassent de fichier écriture I/O avec les résultats de cet appel.

format_hash(book_id, fmt)[source]

Renvoie le hash du format spécifié pour le livre spécifié. Le type de hash est dépendant du système principal, mais il est habituellement SHA-256.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[source]

Renvoie le chemin, la taille et mtime pour le format spécifié. Vous ne devriez presque jamais utiliser path à moins que vous n’en ayez absolument besoin, dès qu’il est accédé il compromet directement la promesse threadsafe de cette API. A la place utilisez la méthode copy_format_to().

Paramètres:

- allow_cache – Si Vrai les valeurs en cache sont utilisées, autrement un accès lent au système de fichiers est fait. Le cache des valeurs peut être périmé si l’accès est effectué au système de fichier en dehors de cette API.

- update_db – Si Vrai Le champ max_size de la base de données est mis à jour pour ce livre.

formats(book_id, verify_formats=True)[source]

Renvoie le tuple pour tous les formats du livre spécifié. Si verify_format est Vrai, vérifie que les fichiers existent sur le disque.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][source]

Return all item_ids for items that have notes in the specified field or all fields if field_name is None

get_all_link_maps_for_book(book_id)[source]

Renvoie tous les liens pour tous les champs référencés par book_id. Si book_id n’existe pas, la méthode renvoie {}.

Exemple : Supposons que l’auteur A ait un lien X, que l’auteur B ait un lien Y, que le tag S ait un lien F, et la balise T a un lien G. Si le livre 1 a un auteur A et une balise T, cette méthode renvoie {“authors”:{“A”:”X”}, “tags”:{“T”, “G”}}. Si l’auteur du livre 2 n’est ni A ni B et qu’il n’a pas d’étiquette, cette méthode renvoie {}.

Paramètres:

book_id – l’identifiant du livre en question.

Renvoie:

{field: {field_value, link_value}, … pour tous les champs qui ont une valeur de lien non vide pour ce livre

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[source]

Utilisé en interne pour implémenter le Navigateur d’Etiquettes

get_custom_book_data(name, book_ids=(), default=None)[source]

Donne les données pour le nom. Par défaut renvoie les données pour tous les book_ids, passez dans une liste d’ids de livre si vous voulez seulement quelques données. Renvoie un mappage du book_id à la valeur. Si une valeur particulière ne peut être décodée, utilise l’option par défaut pour celle-ci.

get_id_map(field)[source]

Renvoie un mappage des nombres id aux valeurs pour le champ spécifié. Le champ doit être un champ many-one ou many-many, autrement une ValueError est évoquée.

get_ids_for_custom_book_data(name)[source]

Renvoie un ensemble d’ids de livre pour lesquels le nom a des données.

get_item_id(field, item_name)[source]

Renvoie l’id élément pour item_name(insensible à la casse) ou None si non trouvé

get_item_ids(field, item_names)[source]

Renvoie l’id élément pour item_name(sensible à la casse)

get_item_name(field, item_id)[source]

Renvoie le nom d’élément pour l’élément spécifié par item_id dans le champ spécifié. Voir aussi get_id_map().

get_item_name_map(field, normalize_func=None)[source]

Return mapping of item values to ids

get_link_map(for_field)[source]

Renvoie un dictionnaire de liens pour le champ fourni.

Paramètres:

for_field – le nom du champ pour lequel la carte de liens est souhaitée

Renvoie:

{field_value:link_value, …} pour les liens non vides

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[source]

Renvoie les métadonnées pour le livre identifié par book_id comme un un objet calibre.ebooks.metadata.book.base.Metadata . Notez que la liste des formats n’est pas vérifiée. Si get_cover est True, la couverture est retournée, soit un chemin au fichier temporaire comme mi.cover ou si cover_as_date est True comme mi.cover_data.

get_next_series_num_for(series, field='series', current_indices=False)[source]

Renvoie les index de séries suivants pour les séries spécifiées, prendre en compte les préférences variées qui contrôlent la génération du numéro suivant de séries.

Paramètres:

- field – Le champ series-like (par défaut à la colonne séries intégrées)

- current_indices – Si Vrai, renvoie à la place un mappage de book_id à l’actuelle valeur series_index.

get_notes_resource(resource_hash) → dict | None[source]

Return a dict containing the resource data and name or None if no resource with the specified hash is found

get_proxy_metadata(book_id)[source]

Semblable à get_metadata() excepté qu’il renvoie un objet ProxyMedadata qui lit uniquement les valeurs depuis la base de données à la demande. Ceci est beaucoup plus rapide que get_metadata quand seulement un petit nombre de champs ont besoin d’être accédés depuis l’objet métadonnées renvoyé.

get_usage_count_by_id(field)[source]

Renvoie un mappage de l’id au compte d’utilisation pour toutes les valeurs du champ spécifié, qui doit être un champ many-one ou many-many.

has_book(mi)[source]

Renvoie True si et seulement si la base de donnée contient une entrée avec le même titre que l’objet Metadata transmis. La comparaison est sensible à la casse. Voir aussi data_for_has_book().

has_format(book_id, fmt)[source]

Renvoie Vrai si et seulement si le format existe sur le disque

has_id(book_id)[source]

Renvoie Vrai si et seulement si le book_id spécifié existe dans la base de données.

import_note(field, item_id, path_to_html_file, path_is_data=False)[source]

Import a previously exported note or an arbitrary HTML file as the note for the specified item

init()[source]

Initialiser ce cache avec des données depuis le système principal.

items_with_notes_in_book(book_id: int) → dict[str, dict[int, str]][source]

Return a dict of field to items that have associated notes for that field for the specified book

link_for(field, item_id)[source]

Return the link, if any, for the specified item or None if no link is found

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][source]

Obtenir des informations sur les fichiers supplémentaires dans le répertoire du livre

Paramètres:

- book_id – l’identifiant de livre dans la base de données pour ce livre

- pattern – le modèle de noms de fichiers à rechercher. Le modèle vide correspond à tous les fichiers supplémentaires. Les modèles doivent utiliser / comme séparateur. Utilisez la constante DATA_FILE_PATTERN pour faire correspondre les fichiers à l’intérieur du répertoire de données.

Renvoie:

Un n-uplet de tous les fichiers supplémentaires correspondant au modèle spécifié. Chaque élément du tuple est ExtraFile(relpath, file_path, stat_result). Où relpath est le chemin relatif du fichier vers le répertoire du livre en utilisant / comme séparateur. stat_result est le résultat de l’appel à os.stat() sur le fichier.

merge_extra_files(dest_id, src_ids, replace=False)[source]

Fusionnez les fichiers supplémentaires de src_ids dans dest_id. Les fichiers en conflit sont renommés automatiquement sauf si replace=True auquel cas ils sont remplacés.

move_book_from_trash(book_id)[source]

Annuler la suppression d’un livre dans le répertoire de la corbeille

move_format_from_trash(book_id, fmt)[source]

Annuler la suppression d’un format dans le répertoire de la corbeille

multisort(fields, ids_to_sort=None, virtual_fields=None)[source]

Renvoie une liste des ids de livre triés. Si ids_to_sort est Aucun, tout les ids de livre sont retournés.

les champs doivent être une liste de 2-tuples de la forme (field_name, ascending = True ou False). Le champ le plus important est le 2-tuple.

notes_data_for(field, item_id) → str[source]

Return all notes data as a dict or None if note does not exist

notes_for(field, item_id) → str[source]

Return the notes document or an empty string if not found

notes_resources_used_by(field, item_id)[source]

Return the set of resource hashes of all resources used by the note for the specified item

pref(name, default=None, namespace=None)[source]

Renvoie la valeur pour la préférence spécifiée ou la valeur spécifiée comme étant par défaut si la préférence n’est pas paramétrée.

read_backup(book_id)[source]

Renvoie la sauvegarde des métadonnées OPF comme une chaîne de caractères ou Aucun si une telle sauvegarde n’existe pas.

remove_books(book_ids, permanent=False)[source]

Supprimer de la base de données les livres spécifiés par les book_ids et effacer leurs formats de fichier . Si permanent est à False, alors les formats de fichiers sont placés dans la corbeille de la bibliothèque.

remove_formats(formats_map, db_only=False)[source]

Supprimer les formats spécifiés des livres spécifiés.

Paramètres:

- formats_map – Un mappage de book_id à une liste de formats à supprimer du livre.

- db_only – Si Vrai, retirer uniquement l’enregistrement pour le format de la base de données, n’efface pas le fichier de format actuel du système de fichiers.

Renvoie:

Une correspondance entre l’identifiant du livre et l’ensemble des formats effectivement supprimés du système de fichiers pour ce livre.

remove_items(field, item_ids, restrict_to_book_ids=None)[source]

Supprimer tous les éléments dans le champ spécifié avec les ids spécifiés. Renvoie l’ensemble des ids de livres affectés. restrict_to_book_ids est un ensemble optionnel d’ids de livres. Si spécifié les éléments seront uniquement supprimés de ces livres.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[source]

Renommer des fichiers de données supplémentaires

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[source]

Renommer les éléments d’un champ many-one ou many-many comme étiquettes ou séries.

Paramètres:

- change_index – Lors du renommage d’un champ series-like change aussi les valeurs series_index.

- restrict_to_book_ids – Un ensemble optionnel d’ids de livres pour lesquels le renommage est effectué, par défaut à tous les livres.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[source]

Restaure l’entre du livre dans la base de données pour un livre qui existe déjà dans le système de fichiers.

restore_original_format(book_id, original_fmt)[source]

Restaure le format spécifié à partir du ORIGINAL_FORMAT précédemment sauvé, s’il y en a. Renvoie Vrai en cas de réussite. Le ORIGINAL_FORMAT est supprimé après une restauration réussie.

property safe_read_lock

Un verrou en lecture sûr qui ne fait rien si le thread à déjà un verrou en écriture, autrement il acquiert un verrou en lecture.Ceci est nécessaire pour prévenir les DowngradeLockErrors, qui peuvent survenir quand on met à jour le cache de recherche en la présence de colonnes composées. Mettre à jour le cache de recherche préserve un verrou exclusif, mais la recherche d’une colonne composée implique de lire des valeurs de champ par l’intermédiaire de ProxyMetadata qui essaye d’obtenir un verrou partagé. Il peut y avoir d’autres scénarios qui déclenchent ceci également.

Cette propriété renvoie un nouvel objet verrou à chaque accès. Cet objet verrou n’est pas récursif (pour la performance) et doit être uniquement utilisé dans une déclaration with comme with cache.safe_read_lock: autrement de mauvaises choses peuvent survenir.

save_original_format(book_id, fmt)[source]

Sauver une copie du format spécifié comme ORIGINAL_FORMAT, écrase tout ORIGINAL_FORMAT existant.

search(query, restriction='', virtual_fields=None, book_ids=None)[source]

Rechercher dans la base de données pour la requête spécifiée, en renvoyant un ensemble d’ids de livre correspondants.

Paramètres:

- restriction – Une restriction qui est ANDed à la requête spécifique. Notez que ces restrictions sont en cache, dès lors une recherche pour a AND b plus lente que celle avec une restriction b.

- virtual_fields – Utilisé en interne (champs virtuels tels que on_device pour y faire une recherche)

- book_ids – Si n’est pas Aucun, un ensemble d’ids de livre pour les livres sera recherché à la place de rechercher tous les livres.

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)[source]

Search the text of notes using an FTS index. If the query is empty return all notes.

set_conversion_options(options, fmt='PIPE')[source]

les options doivent être une carte de la forme {book_id:conversion_options}

set_cover(book_id_data_map)[source]

Paramétrer la couverture pour ce livre. Les données peuvent être soit une QImage, une QPixmap, un objet fichier ou une chaîne de caractères. Elles peuvent être aussi Aucun, dans quel cas toute couverture existante sera supprimée.

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[source]

Paramétrer les valeurs du champ spécifié par name. Renvoie l’ensemble de tous les ids de livre affectés par le changement.

Paramètres:

- book_id_to_val_map – Mappage des book_ids aux valeurs qui devraient être appliquées.

- allow_case_change – Si Vrai, la casse des champs many-one ou many-many sera changée. Par exemple, si un livre à l’étiquette tag1 et que vous paramétrez l’étiquette pour un autre livre à Tag1 alors les deux livres ont l’étiquette Tag1 si allow_case_change est Vrai, autrement ils ont tous les deux l’étiquette tag1.

- do_path_update – Utilisé en interne, vous ne devriez jamais le changer.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[source]

Définit les liens pour les valeurs des éléments dans le champ. Remarque : cette méthode ne modifie pas les valeurs qui ne figurent pas dans le value_to_link_map

Paramètres:

- field – le nom recherché

- value_to_link_map – dict(field_value:link, …). Notez qu’il s’agit de valeurs et non d’identifiants de champ.

Renvoie:

livres modifiés en définissant le lien

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[source]

Paramétrer les métadonnées pour le livre id depuis l’objet Metadata mi.

En réglant force_changes=True forcera set_metadata à mettre à jour les champs même si mi contient des valeurs vides. Dans ce cas, “Aucun” est distingué de “vide”. Si mi.XXX est Aucun, le XXX n’est pas remplacé, autrement il l’est. Les étiquettes, identifiants, et attributs de couverture sont des cas spéciaux. Étiquettes et identifiants ne peuvent être paramétrés à Aucun aussi seront-ils toujours remplacés si force_change est Vrai. Vous devez vous assurer que mi contient les valeurs que vous voulez que le livre ait.. Les couvertures sont toujours changées si une nouvelle couverture est procurée, mais ne sont jamais supprimées. Aussi notez que force_changes n’a pas d’effet sur les réglages titre ou auteurs.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[source]

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.

set_pref(name, val, namespace=None)[source]

Paramétrer la préférence spécifiée à la valeur spécifiée. Voir aussi pref().

split_if_is_multiple_composite(f, val)[source]

Si f est une clé de recherche de colonne composite et que la colonne est is_multiple, diviser v en valeurs uniques et non vides. La comparaison est sensible à la casse. L’ordre n’est pas préservé. Retourne une list() pour des raisons de compatibilité avec les getters des champs de proxy metadata , par exemple les tags.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[source]

Renvoie les ids de tous les livres ayant l’étiquette tag qui sont plus anciennes que le moment spécifié, la comparaison d’étiquette est insensible à la casse.

Paramètres:

- delta – Un objet timedelta ou Aucun. Si Aucun, alors tous les ids avec l’étiquette sont retournées.

- must_have_tag – Si pas Aucun la liste des correspondances sera restreinte aux livres qui ont cette étiquette

- must_have_authors – Une liste d’auteurs. Si pas Aucun la liste des correspondances sera restreinte aux livres qui ont ces auteurs (insensible à la casse).

unretire_note_for(field, item_id) → int[source]

Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database

user_categories_for_books(book_ids, proxy_metadata_map=None)[source]

Retourne les catégories utilisateur pour les livres spécifiés. proxy_metadata_map est optionnel et est utile pour une amélioration de la performance, dans les contextes où un objet ProxyMetadata pour les livres existe déjà. Il devrait être une cartographie des book_ids à leurs objets correspondants ProxyMetadata.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »

- Mise en place d’un environnement de développement calibre »

- Documentation API pour l’outil d’édition de livre numérique

Documentation API pour l’outil d’édition de livre numérique

Les outils d’édition de livre numérique consistent en un objet calibre.ebooks.oeb.polish.container.Container qui représente un livre comme une collection d’HTML + des fichiers ressources, et différents outils qui peuvent être utilisés pour effectuer des opérations sur le conteneur. Tous les outils sont sous forme de fonctions de niveau de module dans les divers modules de calibre.ebooks.oeb.polish.*

Vous obtenez un objet de conteneur pour un livre à un chemin comme ceci:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

Si vous écrivez une extension pour l”`Éditeur de livre numérique, vous obtenez le conteneur actuel pour le livre étant édité comme ceci:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
report_error # No book has been opened yet

L’objet Conteneur
class calibre.ebooks.oeb.polish.container.Container(rootpath, opfpath, log, clone_data=None)[source]

Un conteneur représente un livre numérique ouvert comme un dossier remplit de fichiers et un fichier OPF. Il y a deux concepts importants :

- Le répertoire racine. Ceci est la base du livre numérique. Tous les fichiers des livres numériques sont dans ce dossier ou dans ses sous-dossiers.

- Noms : Ce sont des chemins vers les fichiers de livres relatifs au dossier racine. Ils contiennent toujours des séparateurs POSIX et sont non cotés. Ils peuvent être considérés en tant qu’identificateurs conformes pour les fichiers dans le livre. La plupart des méthodes sur l’objet conteneur fonctionnent avec des noms. Les noms sont toujours sous la forme normale unicode de NFC.

- Clones : l’objet conteneur soutient le clonage efficace sur disque, qui est employé pour mettre en application des points de contrôle dans l’éditeur de livre numérique. Afin de faire ce travail, vous devriez ne jamais accéder à des dossiers directement sur le système de fichiers. Au lieu de cela, utilisez : meth :raw_data ou open() à la lecture/écriture aux fichiers composants le livre.

En convertissant entre les hrefs et les noms, employez les méthodes fournies par cette classe, elles supposent que tous les hrefs sont cités.

abspath_to_name(fullpath, root=None)[source]

Convertir un fichier absolu vers un nom conforme relatif à root

Paramètres:

root – Le dossier de base. Par défaut la racine de cet objet conteneur est utilisée.

add_file(name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None)[source]

Ajouter un fichier à ce conteneur. Les entrées pour le fichier sont automatiquement créées dans la structure organisationnelle et la structure OPF (si le fichier est un document texte)

add_name_to_manifest(name, process_manifest_item=None)[source]

Ajouter une entrée au manifeste pour le fichier avec le nom spécifié. Renvoie l’id du manifeste.

add_properties(name, *properties)[source]

Ajouter les propriétés spécifiées à l’élément du manifeste identifié par le nom.

apply_unique_properties(name, *properties)[source]

Assure que les propriétés spécifiées soient paramétrées uniquement sur l’élément du manifeste spécifié par un nom. Vous pouvez utiliser None comme nom pour supprimer la propriété de tous les éléments.

book_type = 'oeb'

Le type de livre (epub pour les fichiers EPUB et azw3 pour les fichiers AZW3)

commit(outpath=None, keep_parsed=False)[source]

Engager tous les fichiers analysés encrassés vers le système de fichiers et écrire le fichier de livre numérique à outpath.

Paramètres:

- output – Le chemin où écrire le fichier du livre numérique sauvegardé. Si aucun, le chemin du livre original est utilisé.

- keep_parsed – Si True les représentations analysées des éléments engagés sont gardées dans le cache.

commit_item(name, keep_parsed=False)[source]

Soumet un objet analysé au disque (il est numéroté et écrit par rapport fichier sous-jacent). Si keep_parsed est True la représentation analysée est maintenue dans le cache. Voir également : :meth :`parsed `

dirty(name)[source]

Marque l’objet correspondant au nom comme impropre. Voir aussi : parsed().

exists(name)[source]

Vrai si et seulement si un fichier/dossier correspondant au nom de référence existe. Notez que cette fonction souffre des limitations du système de fichiers de l’OS sous-jacent, en particulier l”/la (in)sensibilité à la casse. Aussi sur un système de fichiers non sensible à la casse renvoie Vrai même dans le cas où le nom est différent de la casse du système de fichiers sous-jacent. Voir aussi : has_name()

filesize(name)[source]

Renvoie la taille en bytes du fichier représenté par le nom de référence spécifié. Traite automatiquement les objets impropres analysés. Voir aussi : parsed()

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[source]

Ajoute un article au manifeste avec un href dérivé du nom attribué. Assure automatiquement le caractère unique de l’href et de l’id. Revoie l’article généré.

get_file_path_for_processing(name, allow_modification=True)[source]

Similaire à open() excepté qu’il renvoie un chemin de fichier, au lieu d’un objet fichier ouvert.

property guide_type_map

Mappage du type de guide au fichier de référence

has_name(name)[source]

Renvoie Vrai si un fichier avec le même nom de référence que spécifié existe. Contrairement à exists() cette méthode est toujours sensible à la casse.

href_to_name(href, base=None)[source]

Convertir une href (relative à la base) en un nom. La n-base peut être un nom ou Aucun, dans ce cas self.root est utilisé.

insert_into_xml(parent, item, index=None)[source]

Insérer l’article dans le parent (ou apposez si l’index est Aucun), fixant l’indentation. Travaille seulement avec les articles à fermeture automatique.

is_dir = False

Si ce conteneur représente un livre décompressé (un répertoire)

iterlinks(name, get_line_numbers=True)[source]

Répéter sur tous les liens dans le nom. Si get_line_numbers est Vrai elle produit un résultat dans la forme (link, line_number, offset). Où line_number est le numéro de ligne où le lien intervient et offset est le nombre de caractères depuis le début de la ligne. Notez que offset pourrait actuellement comprendre plusieurs lignes sinon zéro.

make_name_unique(name)[source]

S’assurer que le nom n’est pas déjà présent dans ce livre. Si c’est le cas, renvoyer une version modifiée qui n’existe pas

manifest_has_name(name)[source]

Renvoie True si le manifeste a une entrée correspondante au nom

property manifest_id_map

Mappage de l’id du manifeste aux noms de référence

manifest_items_of_type(predicate)[source]

Les noms de tous les éléments du manifeste dont le media-type correspond à predicate. predicate peut être un paramètre, une liste, une chaîne ou une fonction comprenant un simple argument, qui sera appelé avec le media-type.

manifest_items_with_property(property_name)[source]

Tous les les éléments du manifeste qui ont la propriété spécifiée

property manifest_type_map

Mappage du media-type du manifeste pour lister les noms de référence de ce media-type

property mi

Les métadonnées de ce livre comme un objet Metadata. Notez que cet objet est construit au vol chaque fois que cette propriété est requise, aussi utilisez-la de façon modérée.

name_to_abspath(name)[source]

Convertir un nom de référence en un chemin absolu dépendant de l’OS

name_to_href(name, base=None)[source]

Convertir un nom en une href relative à la base, qui doit être un nom ou Aucun dans quel cas self.root est utilisé comme la base

property names_that_must_not_be_changed

Ensemble de noms qui ne doivent jamais être renommés. Dépend du format de fichier du livre numérique.

property names_that_must_not_be_removed

Ensemble de noms qui ne doivent jamais être supprimés du conteneur. Dépend du format de fichier du livre numérique.

property names_that_need_not_be_manifested

Ensemble de noms qui ont la possibilité d’être absents du manifeste. Dépend du format de fichier du livre numérique

open(name, mode='rb')[source]

Ouvrir le fichier indiqué par le nom pour lecture/écriture directe. Notez que ceci compromettra le fichier s’il est sali et le retire du cache d’analyse. Vous devez terminer avec ce fichier avant d’encore accéder la version analysée, ou de mauvaises choses pourraient se produire.

property opf

Le fichier OPF analysé

opf_get_or_create(name)[source]

Méthode commode pour soit récupérer le premier élément XML avec le nom spécifique ou le créer sous l’élément opf:package puis le renvoyer, s’il n’existe pas déjà.

property opf_version

La version du <package> de l’élément OPF

property opf_version_parsed

La version paramétrée dans l’élément <package> d’OPF à un tuple de nombres entiers

opf_xpath(expr)[source]

Méthode commode pour évaluer une expression Xpath du fichier OPF, dont les préfixes d’espace de nom opf: et dc: ont été prédéfinis.

parsed(name)[source]

Renvoie une représentation analysée du fichier spécifié par le nom. Pour l’HTML et le XML un arbre |xml| est renvoyé. Pour les CSS une feuille de style css_parser est renvoyée. Notez que ces objets analysez sont mis en cache pour la performance. Si vous faites n’importe quel changement à l’objet analysé, vous devez appeler dirty() ainsi le conteneur sait qu’il doit mettre à jour le cache. Voir aussi replace().

raw_data(name, decode=True, normalize_to_nfc=True)[source]

Renvoyer les données brutes correspondant au fichier spécifié par le nom

Paramètres:

- decode – Si Vrai et le fichier à un type MIME basé sur du texte, le décode et renvoie un objet unicode à la place de bytes brutes.

- normalize_to_nfc – Si vrai l’unicode renvoyé est normalisé dans la forme normale NFC comme il est requit pour les formats EPUB et AZW3.

relpath(path, base=None)[source]

Convertir un chemin absolu (avec les séparateurs OS) en un chemin relatif à la pase (par défaut à self.root). Le chemin relatif n’est « pas » un nom. Utiliser abspath_to_name() pour cela.

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[source]

Supprimer les articles spécifiés (par nom de référence) de la structure organisationnelle. Si remove_if_no_longer_in_spine est à Vrai, les articles sont aussi supprimés du livre. Pas uniquement de la structure organisationnelle.

remove_from_xml(item)[source]

Supprime l’article du parent, fixant l’indentation (fonctionne seulement avec les articles à fermeture automatique)

remove_item(name, remove_from_guide=True)[source]

Supprimer l’article spécifié par le nom de ce conteneur. Ceci supprime toutes les références de l’article dans le manifeste, le guide, la structure organisationelle de l’OPF tout comme tout cache interne.

rename(current_name, new_name)[source]

Renomme un fichier de current_name à new_name. Il redirige automatiquement tous les liens dans le fichier si le dossier où ce trouve le fichier change. Notez cependant, ces liens ne sont pas mis à jour dans les autres fichiers qui peuvent référencer ce fichier. Ceci est pour la performance, une telle mise à jour devrait être faite une fois, en lot.

replace(name, obj)[source]

Remplacer l’objet analysé correspondant au nom avec obj. qui doit être un objet similaire, par ex. un arbre |xml| pour du HTML/XML ou une feuille de style css_parser pour un fichier CSS.

replace_links(name, replace_func)[source]

Remplacer tous les liens en nom en utilisant replace_func, qui doit être un appelable qui accepte une URL et retourne l’URL remplacée. Il doit aussi avoir un attribut “remplacé’qui doit être paramétré à Vrai si tout remplacement actuel est fini. Des manières commodes de créer de tels appelables sont d’utiliser les classes LinkReplacer et LinkRebaser.

serialize_item(name)[source]

Convertir un objet analysé (identifié par nom de référence) en une chaîne de bytes.Voir aussi : parsed().

set_spine(spine_items)[source]

Régler la structure organisationnelle pour être une spine_items où spine_items est une itération de la forme (nom, linéaire). Soulèvera une erreur si un des noms n’est pas présent dans le manifeste.

property spine_items

Un itérateur rapportant le chemin pour chaque élément dans la structure organisationnelle des livres. Voir aussi : spine_iter and spine_items.

property spine_iter

Un itérateur qui rapporte l’article, nom is_linear pour chaque article dans la structure organisationnelle des livres, l’article est l’élément |xml|, le nom est le nom de fichier de référence et is_linear est Vrai si l’article est linéaire. Voir aussi: spine_names et spine_items.

property spine_names

Un itérateur rapportant le nom et is_linear pour chaque article dans la structure organisationnelle des livres. Voir aussi : spine_iter et spine_items.

Gestion des fichiers composant dans un conteneur

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[source]

Remplacer les liens aux fichiers dans le conteneur. Itératera sur tous les fichiers dans le conteneur et changera les liens spécifiés par ceux-ci.

Paramètres:

- link_map – Un mappage d’un ancien nom de référence vers un nouveau nom de référence. Par exemple : {'images/old.png': 'images/new.png'}

- frag_map – Un appelable qui prend deux arguments (name, anchor) et renvoie une nouvelle ancre. Ceci est utile si vous avez besoin de changer les ancres dans des fichiers HTML. Par défaut, il ne fait rien.

- replace_in_opf – Si Faux, les liens ne sont pas remplacés dans le ficher OPF..

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[source]

Renommer les fichier dans le conteneur, met à jour automatiquement tous les liens vers ceux-ci.

Paramètres:

file_map – Un mappage d’un ancien nom de référence vers un nouveau nom de référence. Par exemple : {'text/chapter1.html': 'chapter1.html'}.

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[source]

Renvoie les répertoires qui sont recommandés pour les noms de fichier donnés. La recommandation est basée sur où la majorité des fichiers du même type sont localisés dans le conteneur. Si aucun fichiers d’un type particulier ne sont présents, le répertoire recommandé est supposé être le répertoire contenant le fichier OPF.

Impression enjolivée et fixation automatique de l’analyse des erreurs

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[source]

Répare toutes les erreurs analysées dans l’HTML représentées par une chaîne en brut. La réparation se fait en utilisant l’algorithme d’analyse HTML5.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[source]

Répare toutes les erreurs analysées dans tous les fichiers HTML dans le conteneur. La réparation se fait en utilisant l’algorithme d’analyse HTML5.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[source]

Sortie enjolivée de l’HTML présentée comme une chaîne en brut.

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[source]

Sortie enjolivée du CSS présenté comme une chaîne en brut.

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[source]

Sortie enjolivée de l’XML présenté comme une chaîne en brut. Si le « nom » est le nom de l’OPF, un OPF-spécifique supplémentaire est réalisé.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[source]

Sortie enjolivée de tous les fichiers HTML/CSS/XML du conteneur.

Gérer les jaquettes de livre

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[source]

Supprimer une jaquette existante, s’il en existe une. Renvoie False si aucune jaquette existante n’est trouvée.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[source]

Soit crée une nouvelle jaquette à partir des métadonnées du livre ou remplace une jaquette existante. Renvoie Vrai si une jaquette existante était remplacée.

Scission et fusion de fichiers

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[source]

Diviser le fichier spécifié par nom à la position spécifiée par loc_or_xpath. La scission migre automatiquement tous les liens et références aux fichiers affectés.

Paramètres:

- loc_or_xpath – Doit être une expression XPath telle que //h:div[@id= »split_here »]. Peut aussi être un « loc » qui est utilisé en interne pour implémenter la scission dans le panneau de prévisualisation.

- before – Si Vrai la scission s’applique avant l’élément identifié autrement après celui-ci.

- totals – Utilisé en interne

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[source]

Scinder le fichier spécifié à de multiples endroits (toutes les balises qui correspondent à l’expression XPath spécifiée). Voir aussi : split(). La scission migre automatiquement tous les liens et références aux fichiers affectés.

Paramètres:

before – Si Vrai la scission s’applique avant l’élément identifié autrement après celui-ci.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[source]

Fusionner les fichiers spécifiés en un seul fichier, migre automatiquement tous les liens et références aux fichiers affectés. Les fichiers doivent tous être des fichiers soit HTML soit CSS.

Paramètres:

- category – Doivent être soit “texte”` pour l’HTML soit 'styles' pour les fichiers CSS

- names – La liste des fichiers à fusionner.

- master – Lequel des fichiers fusionnés est le fichier maître, c’est à dire, le fichier qui restera après la fusion.

Gérer les couvertures

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[source]

Faire correspondre la couverture du livre à l’image indiquée par cover_path.

Paramètres:

- cover_path – Soit le chemin absolu vers un fichier image ou le nom de référence d’une image dans le livre. lors de l’utilisation d’une image dans le livre, vous devez aussi paramétrer les options. Voir ci-dessous.

- report – Un appelable optionnel qui prend un seul argument. Il sera appelé avec les informations à propos des tâches en cours d’exécution.

- options – Aucun ou un dictionnaire qui contrôle comment la couverture est paramétrée. Le dictionnaire peut avoir des entrées keep_aspect : Vrai ou Faux (Préserve le ratio d’aspect des couvertures dans l’EPUB) no_svg : Vrai ou Faux (Utiliser un habillage de couverture SVG dans la page de titre de l’EPUB) existing: Vrai ou Faux (cover_path réfère à une image existante dans le livre)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[source]

Marquer l’image spécifiée comme l’image de couverture.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[source]

Marquer le fichier HTML spécifié comme la page de titre de l’EPUB.

Paramètres:

move_to_start – Si Vrai le fichier HTML est dépalcé au début de la structure organisationnelle.

Travailler avec le CSS

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[source]

Changer une famille de police de ancien_nom à nouveau_nom. Change toutes les occurrences de la famille de police dans les feuilles de style, les balises de styles et les attributs de style. Si l’ancien_nom correspond à une police intégrée, elle est retirée. Vous pouvez paramétrer new_name à Aucun pour supprimer une famille de police au lieu de la changer.

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)[source]

Supprimer toutes les règles CSS inutilisées du livre. Une règle CSS inutilisée est une qui ne correspond pas à tout contenu actuel.

Paramètres:

- report – Un appelable optionnel qui prend un seul argument. Il sera appelé avec les informations à propos des opérations en cours d’exécution.

- remove_unused_classes – Si Vrai, les attibuts de classe HTML qui ne correspondent à aucunes règles CSS sont aussi supprimées.

- merge_rules – Si Vrai, les règles avec des sélecteurs identiques sont fusionnées

- merge_rules_with_identical_properties – Si Vrai, les règles avec des propriétés identiques sont fusionnées

- remove_unreferenced_sheets – Si True, les feuilles de style qui ne sont pas référencées par un contenu sont supprimées

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[source]

Supprimer les propriétés du CSS spécifié de toutes les règles CSS dans le livre.

Paramètres:

- properties – Ensemble de propriétés à supprimer. Par exemple : {'font-family', 'color'}.

- names – Les fichiers desquels supprimer les propriétés. Options par défaut pour tous les fichiers HTML et CSS dans le livre.

Travailler avec la Table des Matières

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[source]

Générer une Table des Matières à partir d’une liste d’expressions XPath. Chaque expresssion dans la liste correspond à un niveau de la TdM générée. Par exemple : ['//h:h1', '//h:h2', '//h:h3'] générera une Table des Matières à trois niveaux à partir des balises <h1>, <h2> et <h3>.

calibre.ebooks.oeb.polish.toc.from_links(container)[source]

Générer une Table des Matières à partir de liens dans le livre.

calibre.ebooks.oeb.polish.toc.from_files(container)[source]

Générer la table des matières à partir de fichiers dans le livre.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[source]

Créer une Table des Matières intégrée (en HTML) à partir d’une Table des Matières NCX existante.

Paramètres:

title – Le titre pour cette table des matières

Outil d’édition de livre
class calibre.gui2.tweak_book.plugin.Tool[source]

Bases : object

La classe de base pour les outils individuels dans l’extension Editer le Livre. Les membres utiles incluent :

- self.plugin: Une référence à l’objet calibre.customize.Plugin auquel cet outil appartient.

- self. boss

- self. gui

Méthodes qui doivent êtres annulées dans les sous classes :

- create_action()

- register_shortcut()

name = None

Paramétrer ceci à un nom unique il sera utiliser comme une clé

allowed_in_toolbar = True

Si Vrai l’utilisateur peut choisir de placer cet outil dans la barre d’outil des extensions

allowed_in_menu = True

Si Vrai l’utilisateur peut choisir de placer cet outil dans le menu des extensions

toolbar_button_popup_mode = 'delayed'

Le mode du menu contextuel (s’il y en a) du bouton de la barre d’outil. Les valeurs possibles sont “delayed”, “instant”, “button”

property boss

L’objet calibre.gui2.tweak_book.boss.Boss . Utiliser pour contrôler l’interface utilisateur.

property gui

La fenêtre principale de l’interface utilisateur

property current_container

Renvoie l’objet actuel calibre.ebooks.oeb.polish.container.Container qui représente le livre en cours d’édition.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[source]

Enregistrer un raccourci clavier qui déclenchera la qaction spécifiée. Ce raccourci clavier deviendra automatiquement personnalisable pat l’utilisateur via la section Raccourcis clavier des préférences de l’éditeur.

Paramètres:

- qaction – Un objet QAction, il sera déclenché quand la combinaison de touches configurée est pressée par l’utilisateur.

- unique_name – Un nom unique pour ce raccourci/action. Il sera utilisé en interne, il ne doit pas être partagé par toutes autres actions dans cette extension.

- default_keys – Une liste des raccourcis clavier par défaut. Si pas spécifié aucuns raccourcis par défaut ne sera paramétré. Si les raccourcis spécifiés ici entre en conflit avec soit les raccourcis intégrés soit les raccourcis d’une configuration utilisateur/d’autres extensions, ils seront ignorés. Dans ce cas, les utilisateurs auront à configurer les raccourcis manuellement via Préférences. Par exemple : default_keys=('Ctrl+J', 'F9').

- short_text – Une courte description optionnelle de cette action. Si pas spécifiée le texte de QAction sera utilisé.

- description – Une description optionnelle plus longue de cette action, il sera utilisé dans les entrées préférences de ce raccourci.

create_action(for_toolbar=True)[source]

Créer une QAction qui sera ajoutée à soit la barre d’outils extensions soit le menu d’extensions dépendant de for_toolbar. Par exemple :

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

Voir aussi

Méthode register_shortcut().

Contrôler l’interface utilisateur de l’éditeur

L’interface utilisateur de l’éditeur de livre numérique est contrôlée par un unique objet global Boss. Il y a beaucoup de méthodes utiles qui peuvent être utilisée dans le code de l’extension pour effectuer des tâches courantes.

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[source]

add_savepoint(msg)[source]

Créer un point de contrôle de restauration avec le nom spécifié en tant que msg

apply_container_update_to_gui(mark_as_modified=True)[source]

Mettre à jour tous les composants de l’interface utilisateur pour refléter les dernières données dans le conteneur actuel du livre.

Paramètres:

mark_as_modified – Si Vrai, le livre sera marqué comme modifié, ainsi l’utilisateur sera averti de le sauvé en quittant.

close_editor(name)[source]

Fermer l’éditeur qui édite le fichier spécifié par le name

commit_all_editors_to_container()[source]

Appliquer toutes les modifications que l’utilisateur a apportées aux fichiers ouverts dans les éditeurs dans le conteneur. Vous devriez appeler cette méthode avant d’effectuer n’importe quelles actions sur le conteneur actuel

property currently_editing

Renvoie le nom du fichier qui actuellement édité ou Aucun s’il n’y a pas de fichier en cours d’édition

edit_file(name, syntax=None, use_template=None)[source]

Ouvrir le fichier spécifié par le nom dans un éditeur

Paramètres:

- syntax – Le type de média du fichier, par exemple, ``”text/html. Si pas spécifié il est estimé à partir de l’extension de fichier.

- use_template – Un modèle pour initialiser l’ouverture de l’éditeur

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[source]

Ouvrez le livre numérique au path pour l’édition. Montrera une erreur si le livre numérique n’est pas dans un format soutenu ou que le livre actuel a des changements non sauvegardés.

Paramètres:

edit_file – Le nom d’un fichier à l’intérieur du livre nouvellement ouvert pour commencer l’édition. Peut aussi être une liste de noms.

rewind_savepoint()[source]

Annule la création précédente d’un point de contrôle de restauration, utile si vous créer un point de contrôle, termine alors l’opération sans changements

save_book()[source]

Sauvegarder le livre. La sauvegarde s’effectue en tâche de fond

set_modified()[source]

Marquer le livre comme ayant été modifié

show_current_diff(allow_revert=True, to_container=None)[source]

Montrer les changements dans le livre depuis sont dernier état au point de contrôle

Paramètres:

- allow_revert – Si Vrai la boîte de dialogue diff aura un bouton qui permet à l’utilisateur d’annuler tous les changements

- to_container – Un objet conteneur pour y comparer le conteneur actuel. Si Aucun, le conteneur du point de contrôle précédent est utilisé

show_editor(name)[source]

Montrer l’éditeur qui édite le fichier spécifié par le name

sync_preview_to_editor()[source]

Synchronise la position du panneau de configuration avec la position actuelle du curseur dans l’éditeur actuel

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- suivant |
- précédent |
- Démarrer »
- Gestion des Droits Numériques (GDN - DRM)

Gestion des Droits Numériques (GDN - DRM)

La Gestion des Droits Numériques (GDN - DRM) est un terme générique désignant les technologies de contrôle d’accès qui peuvent être utilisées par les fabricants de matériel, les éditeurs, les détenteurs de droits d’auteur et les particuliers pour tenter d’imposer des limites à l’utilisation des contenus et des appareils numériques. Il est aussi, parfois, décrit de façon désobligeante comme la gestion des restrictions numériques. Le terme est utilisé pour décrire toute technologie qui empêche les utilisations (légitimes ou non) de contenu numérique qui n’étaient pas souhaitées ou prévues par le fournisseur de contenu. Le terme ne fait généralement pas référence à d’autres formes de protection contre la copie qui peuvent être contournées sans modifier le fichier ou le dispositif, telles que les numéros de série ou les fichiers clés. Il peut également faire référence à des restrictions associées à des cas spécifiques d’œuvres ou de dispositifs numériques. Les technologies GDN tentent de contrôler l’utilisation des supports numériques en empêchant l’accès, la copie ou la conversion vers d’autres formats par les utilisateurs finaux. Voir Wikipedia [https://en.wikipedia.org/wiki/Digital_rights_management].

Qu’est-ce que la GDN implique pour moi personnellement ?

Lorsque vous achetez un livre numérique avec GDN, vous ne le possédez pas vraiment mais vous avez acheté l’autorisation de l’utiliser de la manière qui vous a été dictée par le vendeur. La GDN limite ce que vous pouvez faire avec les livres numériques que vous avez « achetés ». Souvent, les personnes qui achètent des livres avec GDN ne sont pas conscientes de l’étendue de ces restrictions. Ces restrictions vous empêchent de reformater le livre numérique à votre convenance, y compris d’apporter des modifications stylistiques comme l’ajustement de la taille des caractères, bien qu’il existe des logiciels qui vous permettent de faire de telles choses pour les livres non GDN. Les gens sont souvent surpris qu’un livre électronique qu’ils ont acheté dans un format particulier ne puisse pas être converti dans un autre format si le livre numérique est doté de GDN. Ainsi, si vous avez un Amazon Kindle et que vous achetez un livre vendu par Barnes and Nobles, vous devez savoir que si ce livre électronique est doté de GDN, vous ne pourrez pas le lire sur votre Kindle. Notez que je parle d’un livre que vous achetez, pas d’un livre volé ou piraté mais d’un livre ACHETÉ.

Que fait la GDN pour les auteurs ?

Les éditeurs de livres numériques avec GDN soutiennent que la GDN est dans l’intérêt des auteurs, pour protéger leur intégrité artistique et prévenir le piratage. Mais la GDN n’empêche PAS le piratage. Les personnes qui veulent pirater des contenus ou utiliser des contenus piratés le font quand même et réussissent. Les trois principaux systèmes de GDN pour les livres numériques sont aujourd’hui gérés par Amazon, Adobe et Barnes and Noble, et ils ont tous les trois été piratés. Les GDN ne font qu’incommoder les utilisateurs légitimes. On peut affirmer qu’elle nuit en fait aux auteurs, car les personnes qui auraient acheté le livre choisissent de trouver une version piratée car elles ne sont pas prêtes à supporter les GDN. Ceux qui pirateraient en l’absence de GDN le font aussi en sa présence. Pour réitérer, le point essentiel est que les GDN n’empêchent pas le piratage. Les GDN sont donc non seulement inutiles et nuisibles pour les acheteurs de livres numériques, mais aussi un gaspillage d’argent.

GDN et liberté

Bien que le contenu numérique puisse être utilisé pour rendre l’information ainsi que les œuvres créatives facilement accessibles à tous et donner du pouvoir à l’humanité, ce n’est pas dans l’intérêt de certains éditeurs qui veulent éloigner les gens de cette possibilité de liberté simplement pour maintenir leur pertinence dans un monde qui se développe si vite qu’ils ne peuvent pas suivre.

Pourquoi calibre ne soutient-il pas les GDN ?

calibre est un logiciel à libre accès, alors que la GDN est fermée par nature. Si calibre devait permettre d’ouvrir ou de visualiser des fichiers GDN, il pourrait être trivialement modifié pour servir d’outil de suppression des GDN, ce qui est illégal en vertu des lois actuelles. Les logiciels libres et les GDN sont un conflit de principes. Alors que la GDN vise à contrôler l’utilisateur, le logiciel libre vise à donner du pouvoir à l’utilisateur. Les deux ne peuvent tout simplement pas coexister.

Quel est le point de vue de calibre sur les fournisseurs de contenu ?

Nous croyons fermement que les auteurs et autres fournisseurs de contenu doivent être rémunérés pour leurs efforts, mais la GDN n’est pas la solution. Nous développons cette base de données de livres numériques sans DRM provenant de diverses sources pour vous aider à trouver des alternatives sans DRM et pour aider les auteurs et les éditeurs indépendants de livres numériques sans GDN à faire connaître leur contenu. Nous espérons que cela vous sera utile et nous vous demandons de ne pas pirater le contenu mis à votre disposition ici.

Comment puis-je contribuer à la lutte contre les GDN ?

En tant que personne qui lit et achète des livres numériques, vous pouvez contribuer à la lutte contre les GDN. N’achetez pas de livres numériques avec des GDN. Il existe des éditeurs qui publient des livres électroniques sans GDN. Faites un effort pour savoir s’ils proposent le livre numérique que vous recherchez. Si vous aimez les livres de certains auteurs indépendants qui vendent des livres numériques sans GDN et que vous pouvez vous le permettre, faites-leur des dons. C’est de l’argent bien dépensé car leurs livres numériques ont tendance à être moins chers (il peut y avoir des exceptions) que ceux que vous achèteriez auprès d’éditeurs de livres avec GDN et ils fonctionneraient probablement sur tous les appareils que vous posséderiez à l’avenir, ce qui vous éviterait d’acheter à nouveau le livre numérique. Ne découragez pas les éditeurs et les auteurs de livres numériques sans GDN en piratant leur contenu. Les fournisseurs de contenu méritent une compensation pour leurs efforts. Ne les punissez pas pour avoir essayé d’améliorer votre expérience de lecture en mettant à disposition des livres numériques sans GDN. À long terme, cela vous sera préjudiciable. Si vous avez acheté des livres à des vendeurs qui proposent à la fois des livres avec et sans GDN, ne sachant pas s’ils proposent des GDN ou ne se souciant pas de laisser un commentaire ou une critique sur le site web pour informer les futurs acheteurs de son statut de GDN. De nombreux vendeurs ne pensent pas qu’il soit important d’indiquer clairement à leurs acheteurs si un livre numérique comporte ou non des GDN. Vous trouverez ici <https://www.defectivebydesign.org/guide/ebooks>` un guide pour vivre sans GDN.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- précédent |
- Démarrer »
- Glossaire

Glossaire

RSS

** RSS ** * (Really Simple Syndication - Syndication Vraiment Simple) * est un format d’alimentation Web qui est employé pour publier le contenu fréquemment mis à jour, comme des articles d’actualités, des courriers de blog, etc. C’est un format qui est particulièrement adapté à la lecture par des ordinateurs, et est donc le moyen privilégié d’entrer du contenu du web dans un livre numérique. Il y a beaucoup d’autres formats d’alimentation en service sur l’Internet, et calibre comprend la plupart d’entre eux. En particulier, il a une bonne prise en charge du format ATOM, qui est utilisé généralement pour des blogs.

Recette

Une recette est un ensemble d’instructions qui apprennent à calibre comment convertir une source d’actualités en ligne, comme un magazine ou un blog, en un livre numérique. Une recette est essentiellement du code Python [https://www.python.org]. En soi, il est capable de convertir n’importe quelle source complexe d’actualités en livres numériques. Au niveau le plus simple, c’est juste un ensemble de variables, tels que des URLs, qui donnent à calibre assez d’informations pour aller sur Internet et télécharger les actualités.

HTML

HTML (Hyper Text Mark-Up Language - Langage de Balisage HyperTexte), un sous-ensemble de Standard Generalized Mark-Up Language (SGML) pour l’édition électronique, c’est le standard spécifique utilisé pour le World Wide Web.

CSS

CSS (Cascading Style Sheets - Feuilles de Style en Cascade) est un langage utilisé pour décrire comment un document HTML devrait être restitué (esthétique visuelle).

API

API (Application Programming Interface - Interface de Programmation d’Application) est une interface de code source qu’une bibliothèque fournit aux demandes de soutien pour que les services y soient réalisés par des programmes informatiques.

LRF

LRF Le format de livre numérique qui est lu par les lecteurs de livre numérique SONY.

URL

URL (Uniform Resource Locator- Localisateur de Ressources Uniformes) par exemple : http://example.com

regexp

Les expressions régulières fournissent des moyens concis et flexibles pour identifier des chaînes de texte d’intérêt comme des caractères particuliers, des mots ou des modèles de caractères. Voir le tutoriel pour la syntaxe des expressions régulières utilisées dans Python.

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Index des modules Python

Index des modules 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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Vue d'ensemble : code du module

Modules pour lesquels le code est disponible

- 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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.customize

Code source de 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

[docs]
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

[docs]
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

[docs]
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()

[docs]
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()

[docs]
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()

[docs]
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

[docs]
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()

[docs]
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)

[docs]
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)

# }}}

[docs]
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')

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

# }}}

[docs]
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

[docs]
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

# }}}

[docs]
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

[docs]
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

# }}}

[docs]
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

[docs]
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()

[docs]
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')

# }}}

[docs]
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

[docs]
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

# }}}

[docs]
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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.customize »
- calibre.customize.conversion

Code source de 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

[docs]
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()

[docs]
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()

[docs]
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

[docs]
def postprocess_book(self, oeb, opts, log):
'''
Called to allow the input plugin to perform postprocessing after the book has been parsed.
'''
pass

[docs]
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

[docs]
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)

[docs]
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()

[docs]
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:')

[docs]
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

[docs]
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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.db.cache

Code source de 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 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, timestampfromdt, 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'})

[docs]
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
# }}}

[docs]
@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 {{{

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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

[docs]
@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

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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))

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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 {{{

[docs]
@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)

[docs]
@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

[docs]
@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

[docs]
@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)

[docs]
@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 ()

[docs]
@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()

[docs]
@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)

[docs]
@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]))

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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]

[docs]
@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

[docs]
@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}

[docs]
@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()}

[docs]
@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}

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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()

[docs]
@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

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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

[docs]
@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

[docs]
@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()

[docs]
@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)

[docs]
@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)

[docs]
@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))

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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 dirtieddef _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

[docs]
@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[docs]
@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

[docs]
@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)

[docs]
@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)

[docs]
@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)}

[docs]
@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

[docs]
@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

[docs]
@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

[docs]
@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

[docs]
@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

[docs]
@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

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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

[docs]
@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')

[docs]
@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}

[docs]
@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)

[docs]
@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[docs]
@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))

[docs]
@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

[docs]
@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}

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@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)

[docs]
@write_api def expire_old_trash(self):
' Expire entries from the trash that are too old '
self.backend.expire_old_trash()

[docs]
@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

[docs]
@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

[docs]
@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 mtime = mdata.get('mtime')
if mtime is not None:
mtime = timestampfromdt(mtime)
with exporter.start_file(key, mtime=mtime) as dest:
self._copy_format_to(book_id, fmt, dest)
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):
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)}

[docs]
@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

[docs]
@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

[docs]
@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

[docs]
@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 importer.save_file(metadata['metadata.db'], 'metadata.db for ' + library_path, os.path.join(library_path, 'metadata.db'))
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 importer.save_file(metadata['full-text-search.db'], 'full-text-search.db for ' + library_path, os.path.join(library_path, 'full-text-search.db'))
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 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 if importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
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 fmt_key_map.items():
if fmt == '.cover':
with 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 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 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 importer.corrupted_files:
raise ValueError('Corrupted files:\n' + '\n'.join(importer.corrupted_files))
if progress is not None:
progress(_('Completed'), total, total)
return cache
# }}}

© Copyright Kovid Goyal.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.devices.interface

Code source de 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

[docs]
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

[docs]
@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

[docs]
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

[docs]
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()

[docs]
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()

# }}}

[docs]
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()

[docs]
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

[docs]
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

[docs]
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()

[docs]
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()

[docs]
def post_yank_cleanup(self):
'''
Called if the user yanks the device without ejecting it first.
'''
raise NotImplementedError()

[docs]
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()

[docs]
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()

[docs]
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 {}

[docs]
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()

[docs]
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()

[docs]
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()

[docs]
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()

[docs]
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()

[docs]
@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()

[docs]
def delete_books(self, paths, end_session=True):
'''
Delete books at paths on device.
'''
raise NotImplementedError()

[docs]
@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()

[docs]
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()

[docs]
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()

[docs]
@classmethod def config_widget(cls):
'''
Should return a QWidget. The QWidget contains the settings for the device interface
'''
raise NotImplementedError()

[docs]
@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()

[docs]
@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()

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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()

[docs]
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()

[docs]
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 {}

[docs]
def set_user_blacklisted_devices(self, devices):
'''
Set the list of device uids that should be ignored by this driver.
'''
pass

[docs]
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()

[docs]
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.

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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))

[docs]
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

[docs]
def supports_collections(self):
''' Return True if the device supports collections for this book list. '''
raise NotImplementedError()

[docs]
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()

[docs]
def remove_book(self, book):
'''
Remove a book from the booklist. Correct any device metadata at the same time
'''
raise NotImplementedError()

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.devices.usbms.cli

Code source de 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()

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.devices.usbms.device

Code source de 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

[docs]
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

[docs]
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)

[docs]
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

[docs]
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

[docs]
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)

[docs]
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

[docs]
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

[docs]
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
# ------------------------------------------------------

[docs]
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)

[docs]
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 = {}

[docs]
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

[docs]
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)

[docs]
def filename_callback(self, default, mi):
'''
Callback to allow drivers to change the default file name set by :meth:`create_upload_path`.
'''
return default

[docs]
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

[docs]
def get_annotations(self, path_map):
'''
Resolve path_map to annotation_map of files found on the device
'''
return {}

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.devices.usbms.driver

Code source de 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.
[docs]
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

[docs]
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)

[docs]
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

[docs]
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[docs]
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])))

[docs]
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

[docs]
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

[docs]
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)))

[docs]
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
[docs]
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

[docs]
@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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.metadata.book.base

Code source de 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(',', '|')

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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

[docs]
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

[docs]
def standard_field_keys(self):
'''
return a list of all possible keys, even if this book doesn't have them
'''
return STANDARD_METADATA_FIELDS

[docs]
def custom_field_keys(self):
'''
return a list of the custom fields in this book
'''
return iter(object.__getattribute__(self, '_data')['user_metadata'])

[docs]
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'])))

[docs]
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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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)

[docs]
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('--------------')

[docs]
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 = Nonedef 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)

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.metadata.sources.base

Code source de 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()

[docs]
@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

[docs]
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 {{{

[docs]
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

[docs]
def customization_help(self):
return 'This plugin can only be customized using the GUI'

[docs]
def config_widget(self):
from calibre.gui2.metadata.config import ConfigWidget return ConfigWidget(self)

[docs]
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 {{{

[docs]
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

[docs]
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

[docs]
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]

[docs]
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

[docs]
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 {{{

[docs]
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

[docs]
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

[docs]
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,)

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.container

Code source de 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))
# }}}

[docs]
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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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[docs]
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)

[docs]
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

[docs]
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')

[docs]
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))

[docs]
def name_to_abspath(self, name):
' Convert a canonical name to an absolute OS dependent path '
return name_to_abspath(name, self.root)

[docs]
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))

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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)}

[docs]
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)

[docs]
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

[docs]
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

[docs]
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]

[docs]
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)

[docs]
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)

[docs]
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)

[docs]
def dirty(self, name):
''' Mark the parsed object corresponding to name as dirty. See also: :meth:`parsed`. '''
self.dirtied.add(name)

[docs]
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

[docs]
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

[docs]
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

[docs]
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 itemdef 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'))

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.cover

Code source de 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)

[docs]
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)

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.css

Code source de 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

[docs]
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 > 0def 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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.fonts

Code source de 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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.jacket

Code source de 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)

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.pretty

Code source de 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)

[docs]
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')

[docs]
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')

[docs]
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')

[docs]
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')

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.replace

Code source de 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

[docs]
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

[docs]
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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.split

Code source de 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

[docs]
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

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.ebooks.oeb.polish.toc

Code source de 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

[docs]
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[docs]
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 tocdef 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

[docs]
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

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.gui2.actions

Code source de 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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
def library_changed(self, db):
'''
Called whenever the current library is changed.

:param db: The LibraryDatabase corresponding to the current library.

'''
pass

[docs]
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

[docs]
def initialization_complete(self):
'''
Called once per action when the initialization of the main GUI is completed.
'''
pass

[docs]
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()

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.gui2.preferences

Code source de 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

[docs]
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

[docs]
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()

[docs]
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()

[docs]
def restore_defaults(self):
'''
Should set all config values to their defaults.
'''
pass

[docs]
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

[docs]
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

[docs]
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

[docs]
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 = {}

[docs]
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

[docs]
def initialize(self):
for setting in self.settings.values():
setting.initialize()

[docs]
def commit(self, *args):
restart_required = False for setting in self.settings.values():
rr = setting.commit()
if rr:
restart_required = True return restart_required

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.gui2.tweak_book.boss

Code source de 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

[docs]
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())

[docs]
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')

[docs]
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 '...'))

[docs]
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()

[docs]
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

[docs]
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)

# }}}

[docs]
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)

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.gui2.tweak_book.plugin

Code source de 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

[docs]
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()

[docs]
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'))

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.utils.formatter_functions

Code source de 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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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]

[docs]
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

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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)

[docs]
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)

[docs]
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)))

[docs]
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)))

[docs]
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)))

[docs]
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))

[docs]
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])

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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

[docs]
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 ''

[docs]
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)

[docs]
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)

[docs]
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)

[docs]
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_)]

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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()

[docs]
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

[docs]
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

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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()

[docs]
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

[docs]
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

[docs]
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]))

[docs]
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)

[docs]
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 ''

[docs]
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 ''

[docs]
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()

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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))

[docs]
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

[docs]
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

[docs]
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()

[docs]
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()

[docs]
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)

[docs]
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)

[docs]
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()

[docs]
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()

[docs]
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)

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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 ''

[docs]
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'

[docs]
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 ''

[docs]
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'

[docs]
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())

[docs]
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())

[docs]
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])

[docs]
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())

[docs]
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)

[docs]
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)

[docs]
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"))

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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')

[docs]
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)))

[docs]
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)))

[docs]
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)

[docs]
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)

[docs]
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()

[docs]
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()

[docs]
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

[docs]
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)

[docs]
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()

[docs]
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()

[docs]
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)

[docs]
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)

[docs]
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()

[docs]
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)

[docs]
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()

[docs]
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()

[docs]
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 ""

[docs]
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')

[docs]
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))

[docs]
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()

[docs]
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()

[docs]
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()

[docs]
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 ''

[docs]
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()

[docs]
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()

[docs]
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)

[docs]
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()

[docs]
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)[docs]
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)[docs]
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)

[docs]
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)

[docs]
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)

[docs]
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)

[docs]
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.
Dernière mise à jour mai 03, 2024

Navigation

- modules
- Démarrer »
- Code du module »
- calibre.web.feeds.news

Code source de 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

[docs]
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)

[docs]
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()

[docs]
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

[docs]
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)

[docs]
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)

[docs]
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

[docs]
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

[docs]
@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()

[docs]
@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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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'))

[docs]
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

[docs]
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

[docs]
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

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

[docs]
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('/'))})

[docs]
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)[docs]
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

[docs]
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

[docs]
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()

[docs]
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)

[docs]
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()

[docs]
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)

[docs]
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

[docs]
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

[docs]
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

[docs]
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

[docs]
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)

[docs]
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))

[docs]
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[docs]
@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)

[docs]
@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.
Dernière mise à jour mai 03, 2024

Table of Contents

Manuel de l’Utilisateur calibre

L’Interface Graphique Utilisateur

1. Actions

1. Ajouter des livres
2. Modifier les métadonnées
3. Convertir des livres
4. Visualiser
5. Envoyer vers le périphérique
6. Récupérer des informations
7. Bibliothèque
8. Périphérique connecté
9. Enregistrer sous…
10. Connecter/partager
11. Supprimer des livres

2. Préférences
3. Catalogues
4. Rechercher et trier
5. L’interface de recherche
6. Recherches sauvegardées

7. Recherche dans le texte intégral de tous les livres

8. Bibliothèques virtuelles

9. Marquage temporaire des livres

10. Deviner les métadonnées à partir des noms de fichier

11. Détails du livre
12. Navigateur d’étiquettes
13. Grille de couvertures
14. Navigateur de couvertures

15. Ajouter des notes pour les auteurs, les séries, etc.

16. Affichage rapide
17. Tâches
18. Raccourcis clavier

Ajouter votre site web favori d’actualités

1. Récupération entièrement automatique

1. Le blog calibre

2. bbc.co.uk

2. Personnaliser le processus de récupération

1. Utilisation de la version imprimable de bbc.co.uk

2. Remplacer les styles d’article
3. Découper et émincer
4. Exemple de la vie réelle

3. Astuces pour développer de nouvelles recettes

4. Lectures recommandées

5. Documentation de l’API

1. Documentation API pour les recettes

1. BasicNewsRecipe

La visionneuse de livre numérique

1. Démarrer la Visionneuse de livre numérique

2. Naviguer dans un livre numérique

1. Signets
2. Table des Matières
3. Navigation par emplacement
4. Mode référence

3. Mise en évidence du texte
4. Lire à voix haute
5. Recherche dans le texte

6. Suivre les liens en utilisant uniquement le clavier

7. Personnalisation de l’aspect et de la convivialité de votre expérience de lecture

8. Consultation du dictionnaire
9. Copier du texte et des images
10. Zoom sur les images
11. Raccourcis clavier
12. Contenu non fluide

13. Concevoir votre livre pour qu’il fonctionne bien avec la visionneuse de calibre

Conversion de livre numérique

1. Introduction

2. Apparence

1. Polices
2. Texte
3. Mise en page
4. Esthétique
5. Transformer les styles
6. Transformer du HTML

3. Mise en page
4. Traitement heuristique
5. Rechercher & remplacer

6. Détection de la structure

1. Chapitres et sauts de page

2. Divers

7. Table des matières

8. Utiliser des images comme titre de chapitre en convertissant des documents sources en HTML.

9. Utiliser les attributs de balise pour le texte des entrées dans la Table des Matières

10. Comment les options sont définies/sauvegardées pour la conversion

11. Conseil spécifique au format

1. Convertir des documents Mircosoft Word

1. Des fichiers .doc plus ancien

2. Convertir des documents TXT
3. Conversion de documents PDF
4. Collections de Bandes Dessinées
5. Démonstration de formatage d’EPUB avancée
6. Convertir des documents ODT

7. Convertir en PDF

1. En-têtes et pieds de page

2. Table des Matières imprimable

3. Marges de page personnalisées pour les fichiers HTML individuels

Éditer des livres numériques

Fonctionnement de base

L’Explorateur de fichiers

1. Renommer des fichiers
2. Fusionner des fichiers
3. Ordre de fichier texte changeant
4. Marquage de la couverture
5. Suppression des fichiers
6. Exportation de fichiers

7. Ajout de nouvelles images/polices/etc. ou création de nouveaux fichiers vides

8. Remplacement des fichiers

9. Lier des feuilles de style aux fichiers HTML efficacement

Rechercher & remplacer

1. Recherches sauvegardées
2. Mode fonction
3. Recherche ignorant les balises HTML

Outils automatisés

1. Edition de la Table des Matières
2. Vérification du livre
3. Ajout d’une couverture
4. Intégration des polices référencées
5. Créer un sous-ensemble de polices
6. Amélioration de la ponctuation
7. Transformation des propriétés CSS

8. Suppression en cours des règles CSS inutilisées

9. Correction de l’HTML
10. Embellissement des fichiers
11. Insertion d’une Table des Matières
12. Configuration de Sémantique
13. Filtrage des informations de style
14. Mise à jour des éléments du livre

Points de Contrôle

Le Panneau de prévisualisation directe

1. Fractionnement des fichiers HTML

Le panneau CCS en temps réel

Outils divers

1. La vue Table des Matières

2. Vérification de l’orthographe des mots dans le livre

1. Ajouter de nouveaux dictionnaires

3. Insérer des caractères spéciaux
4. La vue inspecteur de code
5. Vérifications des liens externes
6. Téléchargement de ressources externes

7. Organisation des fichiers par type dans des dossiers

8. Importation de fichiers dans d’autres formats de livres numériques comme l’EPUB

9. L’outil Rapports

Fonctionnalités spéciales dans l’éditeur de code

1. Mise en évidence de la syntaxe
2. Aide sensible au contexte
3. Auto-complétion
4. Fragments

Le Serveur de contenu calibre

1. Accéder au Serveur de contenu depuis d’autres périphériques

1. Accès au serveur à partir de périphériques sur votre réseau domestique

1. Dépannage de la connexion sur un réseau domestique

2. Accès au serveur depuis n’importe où sur Internet

2. L’interface du serveur

1. La liste de livres

2. Le lecteur de livre

3. Compatibilité du navigateur

4. Activer le support hors ligne

5. Gérer les comptes utilisateurs uniquement à partir de la ligne de commande

6. Intégration du Serveur de contenu calibre dans d’autres serveurs

1. Utilisation d’un hôte totalement virtuel

2. Utilisation d’un préfixe d’URL

7. Créer un service pour le serveur calibre sur un système Linux moderne

Comparer des livres numériques

1. Comprendre la vue de comparaison

2. Lancement de l’outil de comparaison

1. Comparer deux fichiers de livre numérique

2. Comparaison de ORIGINAL_FMT à FMT

3. Comparer un point de contrôle à l’état actuel du livre tout en l’éditant

Éditer les métadonnées d’un livre numérique

1. Édition des métadonnées d’un livre à la fois

1. Télécharger les métadonnées
2. Gérer les formats de livre
3. Tout à propos des couvertures

2. Édition des métadonnées de plusieurs livres à la fois

1. Rechercher Et Remplacer

2. Téléchargement de métadonnées par lot

3. Ajouter des fichiers de données supplémentaires à un livre

Questions Fréquemment Posées

Conversion de format de livre numérique

1. Quels sont les formats que supporte calibre en entrée/sortie pour la conversion ?

2. Quels sont les meilleurs formats source pour la conversion ?

3. J’ai converti un fichier PDF, mais le résultat présente divers problèmes ?

4. Comment puis-je convertir mon fichier contenant des caractères non-anglais, ou des guillemets intelligents ?

5. Quel est le problème avec la Table des Matières dans les fichiers MOBI?

6. Comment puis-je convertir une collection de fichiers HTML dans un ordre spécifique ?

7. L’EPUB que j’ai produit avec calibre n’est pas valide ?

8. Comment puis-je utiliser certaines les fonctionnalités avancées des outils de conversion ?

Intégration d’un périphérique

1. Quels périphériques calibre supporte-t’il ?

2. Comment puis-je aider à ce que mon périphérique soit pris en charge dans calibre ?

3. Mon périphérique n’est pas détecté par calibre ?

4. Mon périphérique est non standard ou inhabituel. Comment faire pour le connecter ?

5. Comment utiliser calibre avec mon iPad/iPhone/iPod touch ?

1. Utilisation du serveur de contenu

6. Comment utiliser calibre avec mon téléphone/ma tablette Android ou ma Kindle Fire ?

1. En utilisant un cable USB

2. Sans fil

7. Puis-je accéder à mes livres calibre en utilisant le navigateur web sur mon Kindle ou sur un autre périphérique de lecture ?

8. Je ne peux pas envoyer des courriels en utilisant calibre ?

9. Mon appareil est monté en lecture seule dans Linux, donc calibre ne peut pas s’y connecter ?

10. Pourquoi calibre ne prend pas en charge les collections sur Kindle ou les étagères sur Nook ?

11. J’obtiens un message d’erreur lorsque j’essaie d’utiliser calibre avec mon Kobo Touch/Glo/etc.?

12. Les couvertures des livres que j’envoie à ma Kindle apparaissent momentanément et sont ensuite remplacées par une couverture générique ?

13. Les couvertures de mes fichiers MOBI n’apparaissent plus sur mon Kindle pour PC/Kindle pour Android/iPad etc.

14. J’ai transféré des livres vers ma Kindle en utilisant calibre et ils n’y apparaissent pas ?

Gestion de la bibliothèque

1. Où sont stockés les fichiers des livres ?

2. Comment calibre gère-t-il le nom des auteurs et les tris ?

3. Pourquoi Calibre ne me laisse-t-il pas stocker mes livres dans ma propre structure de dossiers ?

4. Pourquoi calibre n’a-t-il pas telle ou telle colonne ?

5. Puis-je avoir une colonne indiquant le format ou le numéro ISBN ?

6. Comment puis-je déplacer mes données calibre d’un ordinateur à un autre ?

7. La liste des livres dans calibre est vide !

8. J’obtiens des erreurs avec ma bibliothèque calibre sur un disque réseau/NAS ?

Divers

1. Amazon arrête la livraison par e-mail des fichiers MOBI ?

2. Je veux que calibre télécharge les actualités de mon site préféré.

3. Pourquoi le nom calibre ?

4. Pourquoi calibre ne montre qu’une partie de mes polices sous macOS ?

5. calibre ne démarre pas sous Windows ?

6. calibre se bloque/plante occasionnellement ?

7. Les outils Visionneuse de livres numériques et Édition de livres de calibre ne fonctionnent pas sous Windows ?

8. Utiliser la visionneuse ou faire n’importe quelle conversion cause une erreur de permission refusée dans Windows

9. Calibre crash/ ne démarre pas sous macOS ?

10. Je n’obtiens qu’un écran noir ou blanc lorsque je lance la visionneuse de livres numériques calibre ?

11. J’ai téléchargé le programme d’installation mais il ne fonctionne pas ?

12. Mon antivirus affirme que calibre est un virus/cheval de troie ?

13. Comment faire une sauvegarde de calibre ?

14. Comment puis-je mettre des livres numériques EPUB achetés avec calibre (ou que dois-je faire avec les fichiers .acsm) ?

15. J’ai une erreur “accès refusé” ?

16. Puis-je avoir la métadonnée commentaire apparaître sur ma liseuse ?

17. Comment obtenir que calibre utilise mon proxy HTTP ?

18. Je voudrais certaines fonctionnalités ajoutées à calibre. Que puis-je faire ?

19. Pourquoi calibre n’a pas de mise à jour automatique ?

20. Quelle est la licence de calibre ?

21. Comment exécuter calibre depuis ma clé USB ?

22. Comment faire fonctionner des éléments de calibre comme le téléchargement d’actualités et le serveur de contenu sur mon propre serveur Linux ?

Tutoriels

Ajouter votre site web favori d’actualités

1. Récupération entièrement automatique

1. Le blog calibre

2. bbc.co.uk

2. Personnaliser le processus de récupération

1. Utilisation de la version imprimable de bbc.co.uk

2. Remplacer les styles d’article
3. Découper et émincer
4. Exemple de la vie réelle

3. Astuces pour développer de nouvelles recettes

4. Lectures recommandées

5. Documentation de l’API

1. Documentation API pour les recettes

Gérer les sous-groupes de livres, par exemple « genre »

1. Configuration
2. Recherche
3. Restrictions
4. Modèles de fonctions utiles

Tutoriel XPath

1. Sélection par nom de balise
2. Sélection par attributs
3. Sélection par contenu de balise
4. Exemple de livre numérique
5. Les fonctions XPath intégrées

Le langage de modèle calibre

1. Modèles de base

2. Mise en forme avancée

3. Utilisation de modèles pour définir des colonnes personnalisées

4. Modèles et tableaux de connexions

5. Utilisation des fonctions dans les modèles - Mode de fonctionnement unique

6. Mode Programme Général

7. Programmes plus complexes dans les expressions modèles - Mode Programme de Modèles

8. Mode Programmation Python

9. Modèles stockés

10. Fournir des informations supplémentaires aux modèles

11. Notes sur la différence entre les modes

12. Fonctions des modèles Python définis par l’utilisateur

13. Notes spéciales pour la sauvergarde/l’envoi des modèles

14. Astuces

15. Référence de la fonction

1. Reference for all built-in template language functions

Tout à propos de l’utilisation des expressions régulières dans calibre

1. Tout d’abord, un mot d’avertissement et un mot d’encouragement

2. Où pouvez-vous utiliser les expressions régulières dans calibre ?

3. Que diable est une expression régulière ?

4. Prendre soin d’expliquer ?

5. Cela ne semble pas trop mauvais. Ensuite ?

6. He, joli ! Cela commence à avoir du sens !

7. Bien, ces caractères spéciaux sont bien jolis et tout, mais qu’en est-il si je veux faire correspondre à un point ou à un point d’interrogation ?

8. Bien, quels sont les ensembles les plus utiles ?

9. Mais si j’avais quelques chaînes variables que je voudrais faire correspondre, les choses deviennent-elles compliquées ?

10. Vous avez manqué…

11. Au début vous disiez qu’il y avait un moyen de rendre les expressions régulières insensibles à la casse ?

12. Je pense que je commence à comprendre ces expressions régulières maintenant… comment dois-je les utiliser dans calibre?

1. Conversions
2. Ajout de livres
3. Edition des métadonnées par lot

13. Mémento

1. Mémento pour la syntaxe regexp

14. Crédits

Écrire vos propres extensions pour étendre les fonctionnalités de calibre

1. Anatomie d’une extension calibre

2. Une extension d’Interface Utilisateur

1. __init__.py
2. ui.py
3. main.py

4. Obtention des ressources à partir du fichier ZIP de l’extension

5. Activation de la configuration utilisateur pour votre extension

3. Extensions d’édition de livre

1. main.py

4. Ajout des traductions à votre extension
5. L’API extension
6. Dépannage des extensions
7. Plus d’exemples d’extensions
8. Partager vos extensions avec les autres

Compositions mathématiques dans des livres numériques

1. Un simple fichier HTML avec des mathématiques

2. Plus d’information

Création de catalogues AZW3 • EPUB • MOBI

1. Sélection des livres à cataloguer
2. Sections incluses
3. Préfixes
4. Livres exclus
5. Exclure les genres
6. Autres options
7. Couvertures de catalogue personnalisées
8. Ressources d’aide supplémentaires

Bibliothèques virtuelles

1. Création de Bibliothèques virtuelles

1. Exemples de Bibliothèques virtuelles utiles

2. Travailler avec des Bibliothèques virtuelles

3. Utilisation des Bibliothèques virtuelles dans les recherches

4. Utilisation des restrictions supplémentaires

Le système d’URL calibre://

1. Passer à une bibliothèque spécifique

2. Montrer un livre spécifique dans calibre

3. Ouvrir un livre spécifique dans la visionneuse de livre numérique à un emplacement spécifique

4. Recherche des livres

5. Ouvrir une fenêtre de détails du livre pour un livre dans une bibliothèque

6. Open the notes associated with an author/series/etc.

7. Codage hexadécimal des paramètres d’URL

Personnaliser calibre

1. Variables d’environnement
2. Ajustements
3. Outrepasser les Icônes, les modèles, etc.

4. Créer votre propre thème d’icônes pour calibre

5. Personnalisation de calibre avec des extensions

Interface en ligne de commande
Commandes documentées
calibre

1. [options]

calibre-customize

1. [options]

calibre-debug

1. [options]

calibre-server

1. [options]

calibre-smtp

1. [options]

calibredb

1. OPTIONS GLOBALES
2. list
3. add
4. remove
5. add_format
6. remove_format
7. show_metadata
8. set_metadata
9. export
10. catalog
11. saved_searches
12. add_custom_column
13. custom_columns
14. remove_custom_column
15. set_custom
16. restore_database
17. check_library
18. list_categories
19. backup_metadata
20. clone
21. embed_metadata
22. search
23. fts_index
24. fts_search

ebook-convert

1. OPTIONS D’ENTRÉE
2. OPTIONS DE SORTIE
3. ASPECT ET COMPORTEMENT
4. TRAITEMENT HEURISTIQUE
5. RECHERCHER ET REMPLACER
6. DÉTECTION DE LA STRUCTURE
7. TABLE DES MATIERES
8. METADONNEES
9. DÉBOGAGE

ebook-edit

1. [options]

ebook-meta

1. [options]

ebook-polish

1. [options]

ebook-viewer

1. [options]

fetch-ebook-metadata

1. [options]

lrf2lrs

1. [options]

lrfviewer

1. [options]

lrs2lrf

1. [options]

web2disk

1. [options]

Commandes non documentées

Mise en place d’un environnement de développement calibre

1. Philosophie de conception

1. Disposition du code

2. Obtenir le code

1. Soumettre vos changements à inclure

3. Environnement de développement Windows
4. Environnement de développement macOS
5. Environnement de développement Linux

6. Avoir des installations séparées de calibre « normale » et « développement » sur le même ordinateur

7. Astuces de dépannage

1. En utilisant les déclarations d’impression

2. Utilisation d’un interpréte Python interactif

3. Utiliser le débogueur de Python comme débogueur à distance

4. Utiliser le débogueur dans votre IDE Python préféré

5. Exécuter des scripts quelconques dans l’environnement Python calibre

8. Utilisation de calibre dans vos projets

1. Installation binaire de calibre

2. Installez les sources sur Linux

9. Documentation API pour différentes parties de calibre

1. Documentation API pour les recettes

1. BasicNewsRecipe

2. Documentation API pour les extensions

1. Extension
2. FileTypePlugin
3. Les extensions métadonnées
4. Extension de catalogue
5. Extension de téléchargement de métadonnées
6. Extension de conversion
7. Pilotes de périphérique
8. Actions interface utilisateur
9. Extensions préférences

3. Documentation API pour l’interface base de données

1. Cache

4. Documentation API pour l’outil d’édition de livre numérique

1. L’objet Conteneur

2. Gestion des fichiers composant dans un conteneur

3. Impression enjolivée et fixation automatique de l’analyse des erreurs

4. Gérer les jaquettes de livre
5. Scission et fusion de fichiers
6. Gérer les couvertures
7. Travailler avec le CSS
8. Travailler avec la Table des Matières
9. Outil d’édition de livre

10. Contrôler l’interface utilisateur de l’éditeur

Gestion des Droits Numériques (GDN - DRM)

1. Qu’est-ce que la GDN implique pour moi personnellement ?

2. Que fait la GDN pour les auteurs ?

3. GDN et liberté

4. Pourquoi calibre ne soutient-il pas les GDN ?

5. Quel est le point de vue de calibre sur les fournisseurs de contenu ?

6. Comment puis-je contribuer à la lutte contre les GDN ?

Glossaire