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

Navigation

- moduler
- nästa |
- Start »
- Användarmanualen för calibre

Användarmanualen för calibre
calibre är en bibliotekshanterare för e-böcker. Den kan visa, konvertera och katalogisera e-böcker i de flesta större e-bokformaten. Den kan också prata med många e-bokläsenheter. Den kan gå ut på internet och hämta metadata för dina böcker. Den kan hämta tidningar och konvertera dem till e-böcker för bekväm läsning. Den är plattformsoberoende som körs på Linux, Windows och macOS.

Du har precis startat calibre. Vad gör du nu? Innan calibre kan göra någonting med dina e-böcker måste den först känna till dem. Dra och släpp några e-bokfiler in i calibre eller klicka på knappen ”Lägg till böcker” och bläddra efter de e-böcker du vill arbeta med. När du har lagt till böckerna kommer de att visas i huvudvyn och se ut ungefär så här:

När du har beundrat listan med böcker som du just lagt till, kommer du antagligen att vilja läsa en. För att göra det måste du konvertera boken till ett format som din läsenhet förstår. När calibre körs första gånger startas välkomstguiden och ställer in calibre för din läsenhet. Konvertering är enkelt. Välj bara den bok du vill konvertera och klicka sedan på knappen ”Konvertera böcker”. Ignorera alla alternativ just nu och klicka på ”OK”. Den lilla ikonen i det nedre högra hörnet kommer att börja snurra. När den har snurrat klart, är din konverterade bok klar. Klicka på knappen ”Visa” för att läsa boken.

Om du vill läsa boken på din läsenhet, anslut läsenheten till datorn, vänta tills calibre upptäcker den (10-20 sekunder) och klicka sedan på knappen ”Överför till enhet”. När ikonen slutar snurra igen, koppla bort din läsenhet och börja läsa! Om du inte konverterade boken i föregående steg konverterar calibre den automatiskt till det format som din läsarenhet förstår.

För att komma igång med mer avancerad användning bör du läsa om Det grafiska användargränssnittet. För ännu mer kraft och mångsidighet, lär dig Kommandoradsgränssnitt. Du kommer även att finna listan med Vanliga frågor användbar.

Om du har ytterligare frågor eller vill diskutera calibre med andra användare eller fråga om hjälp med något specifikt finns forum och andra hjälpresurser tillgängliga [https://calibre-ebook.com/sv/help].

Avsnitt

- Det grafiska användargränssnittet
- Lägga till din favorit nyhetswebbplats
- E-bokvisaren
- Konvertering av e-bok
- Redigera e-böcker
- calibre-innehållsservern
- Jämföra e-böcker
- Redigera metadata för e-böcker
- Vanliga frågor
- Handledningar
- calibre:// URL-schema
- Anpassa calibre
- Kommandoradsgränssnitt
- Inrätta en calibre-utvecklingsmiljö
- Digital Rights Management (DRM)
- Ordlista

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Det grafiska användargränssnittet

Det grafiska användargränssnittet

Det grafiska användargränssnittet (GUI) ger tillgång till alla funktioner för bibliotekshantering och konvertering av e-boksformat. Det grundläggande arbetsflödet för att använda calibre är att först lägga till böcker till biblioteket från din hårddisk. calibre kommer automatiskt att försöka läsa metadata från böckerna och lägga till dem i sin interna databas. När de väl finns i databasen kan du utföra olika Åtgärder på dem som inkluderar konvertering från ett format till ett annat, överföring till läsenheten, visning på din dator och redigering av metadata. Det senare inkluderar modifiering av omslaget, beskrivningen och taggar bland andra detaljer. Observera att calibre skapar kopior av filerna du lägger till i den. Dina originalfiler lämnas orörda.

Gränssnittet är uppdelat i olika avsnitt:

- Åtgärder

- Inställningar

- Kataloger

- Söka & sortera

- Sökgränssnittet

- Spara sökningar

- Sökning i hela texten i alla böcker

- Virtuella bibliotek

- Tillfällig markering av böcker

- Uppskatta metadata från filnamn

- Bokdetaljer

- Taggbläddrare

- Omslagsrutnät

- Omslagsbläddrare

- Adding notes for authors, series, etc.

- Snabbvisning

- Jobb

- Tangentbordsgenvägar

Åtgärder

Åtgärdsverktygsfältet ger praktiska genvägar till vanliga åtgärder. Om du högerklickar på knapparna kan du utföra variationer av standardåtgärden. Observera att åtgärdsverktygsfältet ser lite annorlunda ut beroende på om du har en e-bokläsare ansluten till din dator.

- Lägg till böcker

- Redigera metadata

- Konvertera böcker

- Visa

- Överför till enhet

- Hämta nyheter

- Bibliotek

- Enhet

- Spara till disk

- Ansluta/dela

- Ta bort böcker

Lägg till böcker

Åtgärden Lägg till böcker har sju varianter som nås genom att göra en högerklickning på knappen.

1. Lägg till böcker från en enda mapp: Öppnar en dialogruta för filval och låter dig ange vilka böcker i en mapp bör tilläggas. Den här åtgärden är kontextkänslig, dvs. det beror på vilken mapp du har valt. Om du har valt biblioteket, kommer böcker att läggas till i biblioteket. Om du har valt e-bokläsenheten kommer böckerna överföras till enheten och så vidare.

2. Lägg till böcker från mappar och undermappar: Låter dig välja en mapp. Mappen och alla dess undermappar skannas rekursivt och alla e-böcker som hittas läggs till i biblioteket. Du kan välja om calibre ska lägga till alla filer som finns i en enda mapp till en enda bokpost eller flera bokposter. calibre förutsätter att varje mapp innehåller en enda bok. Alla e-bokfiler i en mapp antas vara samma bok i olika format. Den här åtgärden är inversen av åtgärden Spara till disk, dvs. du kan Spara till disk, ta bort böckerna och lägg tillbaka dem i en bok per mappläge utan att förlora någon information förutom datumet (det här förutsätter att du inte har ändrat någon av inställningarna för åtgärden Spara till disk).

3. Lägg till flera böcker från arkivet (ZIP/RAR): Låter dig lägga till flera e-böcker som är lagrade i de valda ZIP- eller RAR-filerna. Det är en praktisk genväg som innebär att man först slipper att packa upp arkivet och sedan lägga till böckerna via något av ovanstående två alternativ.

4. Lägg till tom bok (Bokpost utan format): Låter dig skapa en tom bokpost. Detta kan användas för att sedan manuellt fylla i information om en bok som du kanske inte har ännu i din samling.

5. Lägg till från ISBN: Låter dig lägga till en eller flera böcker genom att skriva in deras ISBN.

6. Lägg till filer i valda bokposter: Låter dig lägga till eller uppdatera filer som hör till en befintlig bok i ditt bibliotek.

7. Lägg till datafiler till valda bokposter: Låter dig lägga till valfritt antal extra filer som kommer att lagras i en data-underkatalog i bokkatalogen. Se Lägga till extra datafiler i en bok för detaljer.

8. Lägg till en tom fil i valda bokposter: Låter dig lägga till en tom fil i det angivna formatet i de valda bokposterna.

Åtgärden Lägg till böcker kan läsa metadata från en bred variation av e-bokformat. Dessutom försöker den uppskatta metadata från filnamnet. Se sektionen Uppskatta metadata från filnamn för att lära dig hur du anpassar detta.

För att lägga till ett ytterligare format för en befintlig bok kan du göra någon av tre saker:

1. Dra och släpp filen på bokdetaljpanelen på höger sida av huvudfönstret

2. Högerklicka på knappen Lägg till böcker och välj Lägg till filer i valda bokposter.

3. Klicka på knappen Lägg till böcker i övre högra delen av dialogrutan Redigera metadata, nås av åtgärden Redigera metadata.

Redigera metadata

Åtgärden Redigera metadata har fyra variationer som kan nås genom att göra en högerklickning på knappen.

1. Redigera metadata individuellt: Gör att du kan redigera metadata för böcker en efter en med möjlighet att hämta metadata, inklusive omslag, från Internet. Det gör också att du kan lägga till eller ta bort vissa e-bokformat från en bok.

2. Redigera metadata i grupp: Gör det möjligt att redigera vanliga metadatafält för ett stort antal böcker samtidigt. Det fungerar på alla böcker som du har valt i biblioteksvyn.

3. Hämta metadata och omslag: hämtar metadata och omslag (om tillgängliga) för de böcker som är valda i boklistan.

4. Slå samman bokposter: Ger dig möjlighet att slå samman metadata och format av två eller flera bokposter. Du kan välja att antingen ta bort eller behålla posterna som inte klickades först.

5. Hantera datafiler: Hantera de extra datafilerna som är kopplade till de valda böckerna.

För mer detaljer, se Redigera metadata för e-böcker.

Konvertera böcker

E-böcker kan konverteras från ett antal format till vilket format din e-bok läsare föredrar. Många e-böcker tillgängliga för köp kommer att skyddas av Digital Rights Management (DRM)-teknik. calibre kommer inte att konvertera dessa e-böcker. Det är lätt att ta bort DRM från många format, men eftersom detta kan vara olagligt, måste du hitta verktyg för att frigöra dina böcker själv och sedan använda calibre för att konvertera dem.

För de flesta personer borde konvertering vara en enkel uppgift med ett klick. Om du vill lära dig mer om konverteringsprocessen, se Konvertering av e-bok.

Åtgärden Kovertera böcker har tre varianter som nås genom att göra en högerklickning på knappen.

1. Konvertera individuellt: Här kan du ange konverteringsalternativ för att anpassa konverteringen av varje vald e-bok.

2. Gruppkonvertering: Gör att du kan ange alternativ bara en gång för att konvertera ett antal e-böcker i grupp.

3. Skapa en katalog över böckerna i ditt calibre-bibliotek: Gör att du kan skapa en fullständig lista över böckerna i biblioteket, inklusive alla metadata, i flera format som XML, CSV, BiBTeX, EPUB och MOBI. Katalogen kommer att innehålla alla böcker nu visas i biblioteksvyn. Detta gör att du kan använda sökfunktioner för att begränsa de böcker som ska katalogiserade. Dessutom, om du väljer flera böcker med muspekaren, kommer bara dessa böcker att läggas till katalogen. Om du skapar katalogen i ett e-bokformat som EPUB, MOBI eller AZW3, nästa gång du ansluter din e-bokläsenhet kommer katalogen skickas automatiskt till enheten. För mer information om hur kataloger fungerar, läs Skapa kataloger för AZW3 • EPUB • MOBI.

Visa

Åtgärden Visa visar boken i ett e-bokvisarprogram. calibre har en inbyggd visare för många e-bokformat. För andra format använder den operativsystemets standardprogram. Du kan anpassa vilka format ska öppnas med den interna visaren via Inställningar → Gränssnitt → Beteende. Om en bok har fler än ett format, kan du visa ett visst format genom att göra ett högerklick på knappen.

Överför till enhet

Skicka till enhet åtgärden har åtta variationer, som nås genom att göra ett högerklick på knappen.

1. Överför till huvudminne: De valda böckerna överförs till e-bokläsenhetens huvudminne.

2. Överför till minneskort (A): De valda böckerna överförs till minneskortet (A) hos e-bokläsenheten.

3. Överför till minneskort (B): De valda böckerna överförs till minneskortet (B) hos e-bokläsenheten.

4. Överför specifikt format till: De valda böckerna överförs till vald plats på enheten, i det format som du anger.

5. Mata ut enhet: Kopplar bort enheten från calibre.

6. Ange som standard överför till enhet åtgärd: Här kan du ange vilket av alternativen, 1 till 5 ovan eller 7 nedan, kommer att vara standardåtgärd när du klickar på huvudknappen.

7. Överför och ta bort från bibliotek: De valda böckerna överförs till vald plats på enheten och tas sedan bort från biblioteket.

8. Hämta noteringar (experimentellt): Överför noteringar du kan ha gjort på en e-bok på din enhet till kommentarmetadata av boken i calibre-biblioteket.

Du kan kontrollera filnamnet och mappstrukturen för filer som överförs till enheten genom att skapa en mall i Inställningar → Importera/exportera → Överföra böcker till enheter. Se också Mallspråket för calibre.

Hämta nyheter

Åtgärden Hämta nyheter hämtar nyheter från olika webbplatser och konverterar de till en e-bok som kan läsas på din e-bokläsenhet. Normalt, läggs den nyskapade e-boken i ditt e-bokbibliotek, men om en e-bokläsenhet är ansluten vid den tidpunkt då hämtningen är klar, överförs nyheten också till läsenheten automatiskt.

Åtgärden Hämta nyheter använder enkla recept (10-15 rader kod) för varje nyhetsplats. Om du vill veta hur du skapar recept för dina egna nyhetskällor, se Lägga till din favorit nyhetswebbplats.

Åtgärden Hämta nyheter har tre varianter som nås genom att göra en högerklickning på knappen.

1. Schemalägg nyhetshämtning: Låter dig schemalägga hämtningen av dina valda nyhetskällor från en lista med hundratals tillgängliga. Schemaläggning kan ställas in individuellt för varje nyhetskälla du väljer och schemaläggning är flexibel på så sätt att du kan välja specifika veckodagar eller en frekvens av dagar mellan hämtningar.

2. Lägg till en anpassad nyhetskälla: Gör att du kan skapa ett enkelt recept för hämtning av nyheter från en egen nyhetsplats som du vill komma åt. Skapa recept kan vara så enkelt som att ange en RSS flöde-URL, eller så kan du vara mer normativ genom att skapa Pythonbaserad-kod för uppgiften. För mer information, se Lägga till din favorit nyhetswebbplats.

3. Hämta alla schemalagda nyhetskällor: Får calibre att omedelbart börja hämtar alla nyhetskällor som du har schemalagt.

Bibliotek

BIbliotek åtgärden låter dig skapa, växla mellan, byta namn på eller ta bort ett bibliotek. calibre ger dig möjlighet att skapa så många bibliotek som du vill. Du kan till exempel skapa ett skönlitterärtbibliotek, ett faktabibliotek, ett bibliotek för främmande språk, ett projektbibliotek, eller någon struktur som passar dina behov. Biblioteken är den högsta organisationsstrukturen inom calibre. Varje bibliotek har sin egen uppsättning av böcker, taggar, kategorier och lagringsplatsbas.

1. Växla/skapa bibliotek…: Låter dig; a) ansluta till ett befintligt calibre-bibliotek på en annan plats, b) skapa ett tomt biblioteket på en ny plats eller, c) flytta det aktuella biblioteket till en nyligen angiven plats.

2. Snabbväxling: Låter dig växla mellan bibliotek som har registrerats eller skapats inom calibre.

3. Byt namn på bibliotek: Låter dig byta namn på ett bibliotek.

4. Välj en slumpad bok: Väljer en slumpmässig bok i biblioteket för dig

5. Ta bort bibliotek: Gör att du kan avregistrera ett bibliotek från calibre.

6. Exportera/importera alla calibre-data: Låter dig antingen exportera calibre-data för migration till en ny dator eller importera tidigare exporterade data.

7. <biblioteksnamn>: Åtgärder 7, 8 osv… ger dig omedelbar övergång tillgång mellan flera bibliotek som du har skapat eller bifogas. Denna lista innehåller endast de fem mest använda bibliotek. För den kompletta listan, använd snabbväxlingsmenyn.

8. Biblioteksunderhåll: Låter dig kontrollera det aktuella biblioteket för datakonsistensproblem och återställa det aktuella bibliotekets databas från säkerhetskopior.

Observera

Metadata för dina e-böcker, t.ex. titel, författare, och taggar, lagras i en enda fil i din calibre-biblioteksmapp som heter metadata.db. Om filen blir korrupt (mycket ovanligt), kan du förlora metadata. Lyckligtvis säkerhetskopierar calibre automatiskt metadata för varje enskild bok i bokens mapp som en OPF-fil. Genom att använda åtgärden ”Återställ databas” under Biblioteksunderhåll som beskrivs ovan kan du låta calibre återskapa metadata.db-filen från de enskilda OPF-filerna åt dig.

Du kan kopiera eller flytta böcker mellan olika bibliotek (när du har mer än ett bibliotek uppsatt) genom att högerklicka på boken och välja åtgärden Kopiera till bibliotek.

Enhet

Med åtgärden Enhet kan du se böckerna i enhetens huvudminne eller minneskort eller mata ut enheten (ta bort den från calibre). Den här ikonen visas automatiskt i calibre-huvudverktygsfältet när du ansluter en enhet som stöds. Du kan klicka på den för att se böckerna på din enhet. Du kan också dra och släppa böcker från ditt calibre-bibliotek till ikonen för att överföra dem till din enhet. Omvänt kan du dra och släppa böcker från din enhet till biblioteksikonen i verktygsfältet för att överföra böcker från din enhet till calibre-biblioteket.

Spara till disk

Åtgärden Spara till disk har fem varianter som nås genom att göra en högerklickning på knappen.

1. Spara till disk: Sparar de valda böckerna till disken som organiseras i mappar. Mappstrukturen ser ut som:

Author_(sort)
Title
Book Files

Du kan kontrollera filnamnet och mappstrukturen för filer som sparas till disk genom att skapa en mall i Inställningar → Importera/exportera → Spara böcker till disk. Se också Mallspråket för calibre.

2. Spara till disk i en enda mapp: Sparar de valda böckerna till disk i en enda mapp.

För 1. och 2., alla tillgängliga format, samt metadata lagras på disk för varje vald bok. Metadata lagras i en OPF-fil. Sparade böcker kan återimporteras till biblioteket utan förlust av information genom att använda åtgärden Lägg till böcker.

3. Spara endast *<ditt önskade>*-format till disk: Sparar de valda böckerna till disk i mappstrukturen som visas i (1.) men bara i ditt önskade e-bokformat. Du kan ställa in ditt önskade format i Inställningar → Gränssnitt → Beteende → Önskat utmatningsformat

4. Spara endast *<ditt önskade>*-format till disk i en enda mapp: Sparar de valda böckerna till din hårddisk i en enda mapp men bara i ditt önskade e-bokformat. Du kan ställa in ditt favorit format i Inställningar → Gränssnitt → Beteende → Önskat utmatningsformat

5. Spara enstaka format till disk…: Sparar de valda böckerna till disk i mappstrukturen som visas i (1.) men bara i det format du väljer från popup-listan.

Ansluta/dela

Ansluta/dela-åtgärden gör det möjligt att manuellt ansluta till en enhet eller mapp på din dator. Du kan också ställa in ditt calibre-bibliotek för åtkomst via en webbläsare eller e-post.

Ansluta/dela åtgärden har fyra variationer, som nås genom att göra ett högerklick på knappen.

1. Anslut till mapp: Låter dig ansluta till valfri mapp på din dator som om den vore en enhet och använda alla faciliteter calibre har för enheter med den mappen. Användbart om enheten inte kan stödas av calibre, men finns som en USB-disk.

2. Starta innehållsserver: Startar inbyggd calibre-webbserver. Vid start kommer ditt calibre-bibliotek vara tillgänglig via en webbläsare från internet (om du väljer). Du kan anpassa hur webbservern nås genom att ställa in på Inställningar → Delning → Dela via nätet

3. Ställ in e-postbaserad delning av böcker: Tillåter delning av böcker och nyhetsflöden via e-post. När du anpassat e-postadresser för det här alternativet kommer calibre skicka nyhetsuppdateringar och bokuppdateringar till de angivna e-postadresser. Du kan anpassa hur calibre skickar e-post genom att ange inställningar på Inställningar → Delning → Dela böcker via e-post. När du har ställt in en eller flera e-postadresser, kommer menyalternativet ersättas av menyposter för att skicka böcker till de anpassade e-postadresser.

Ta bort böcker

Åtgärden Ta bort böcker tar bort böcker permanent så använd den med omsorg. Det är kontextkänslig, det vill säga att det beror på vilken katalog du har valt. Om du har valt Bibliotek kommer böcker att tas bort från biblioteket. Om du har valt e-bokläsenheten kommer böcker att tas bort från enheten. För att ta bort endast ett visst format för en given bok, använd åtgärden Redigera metadata. Ta bort böcker har också fem variationer som kan nås genom att göra en högerklickning på knappen.

1. Ta bort valda böcker: Tillåter dig att permanent ta bort alla böcker som är valda i boklistan.

2. Ta bort filer av ett specifikt format från valda böcker…: Tillåter dig att permanent ta bort e-böcker med ett särskilt format från böcker som är valda i boklistan.

3. Ta bort alla format från valda böcker, utom…: Tillåter dig att permanent ta bort filer av godtyckligt format förutom ett angivet särskilt format från böcker som är valda i boklistan.

4. Ta bort alla format från valda böcker: Tillåter dig att permanent ta bort filer från böcker som är valda i boklistan. Bara metadata kommer återstå.

5. Ta bort omslag från valda böcker: Tillåter dig att permanent ta bort omslagsbildfiler från böcker som är valda i boklistan.

6. Ta bort matchande böcker från enhet: Tillåter dig att ta bort e-bokfiler från en ansluten enhet som matchar böcker valda i boklistan.

7. Återställ nyligen borttagna: Låter dig ångra borttagningen av böcker eller format.

Observera

Observera att när du använder Ta bort böcker för att ta bort böcker från ditt calibre-bibliotek, tar bort bokposten, men böckerna lagras tillfälligt, under några dagar, i en papperskorg. Du kan ångra borttagningen genom att högerklicka på knappen Ta bort böcker och välja att Återställa nyligen borttagna böcker.

Inställningar

Med åtgärden Inställningar kan du ändra hur olika aspekter av calibre fungerar. Den har fyra variationer, som nås genom att högerklicka på knappen.

1. Inställningar: Låter dig ändra hur olika aspekter av calibre fungerar. Genom att klicka på knappen utförs också denna åtgärd.

2. Kör välkomstguide: Tillåter dig att starta välkomstguiden som dyker upp första gången du startar calibre.

3. Skaffa insticksmoduler för att utöka calibre: Öppnar ett nytt fönster som visar insticksmodul för calibre. Dessa insticksmodul är utvecklade av tredje parter för att utöka calibres funktionalitet.

4. Starta om i felsökningsläge: Låter dig aktivera ett felsökningsläge som kan hjälpa calibre-utvecklarna att lösa problem som du stöter på med programmet. För de flesta användare bör detta förbli inaktiverat såvida inte en utvecklare instruerar att aktivera det.

Kataloger

En katalog är en samling av böcker. calibre kan hantera två typer av olika kataloger:

1. Bibliotek: Detta är en samling böcker lagrade i ditt calibre-biblioteket på din dator.

2. Enhet: Detta är en samling böcker som lagras i din e-bokläsenhet. Det kommer att vara tillgängliga när du ansluter läsenheten till datorn.

Många operationer, som att lägga till böcker, ta bort, visa osv., är sammanhangskänsliga. Så, till exempel om du klickar på knappen Visa när du har valt katalogen Enhet, öppnar calibre filerna på enheten för att visa. Om du har valt katalogen Bibliotek öppnas filer i ditt calibre-bibliotek istället.

Söka & sortera

Söka & sortera avsnittet tillåter dig utföra flera kraftfulla åtgärder med dina boksamlingar.

- Du kan sortera dem efter titel, författare, datum, betyg osv. genom att klicka på kolumntitlarna. Du kan också göra undersortering, dvs. sortera på flera kolumner. Om du till exempel klickar på titelkolumnen och sedan på författarkolumnen kommer boken att sorteras efter författare och och sedan sorteras alla poster för samma författare efter titel.

- Du kan söka efter en viss bok eller en uppsättning böcker med hjälp av sökfältet. Mer om detta nedan.

- Du kan snabbt och bekvämt redigera metadata genom att välja den post som du vill ändra i listan och trycka på tangenten E.

- Du kan utföra Åtgärder på uppsättningar av böcker. För att välja flera böcker kan du antingen:

- Håll Ctrl-tangenten nedtryckt och klicka på de böcker du vill välja.

- Håll Skift-tangenten nedtryckt och klicka på start- och slutbok av en rad böcker som du vill välja.

- Du kan anpassa vilka fält som ska visas med hjälp av dialogrutan Inställningar.

- För att utföra komplex multipelkolumnbaserad undersortering lägg till verktyget Sortera efter till ett verktygsfält via Inställningar → Verktygsfält och menyer.

Sökgränssnittet

Du kan söka i all bokmetadata genom att ange söktermer i sökfältet. Till exempel:

Asimov Foundation format:lrf

Detta kommer att matcha alla böcker i ditt bibliotek som har Asimov och Foundation i deras metadata och finns i LRF-formatet. Några fler exempel:

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

Söktyper

There are four search kinds: contains, equality, regular expression (see regular expressions [https://en.wikipedia.org/wiki/Regular_expression]), and character variant. You choose the search kind with a prefix character.

’Innehåller sökningar

Sökningar utan prefix är ”innehåller” och är som standard skiftlägesokänsliga. Ett objekt matchar om söksträngen visas någonstans i den angivna metadatan. Du kan göra innehåller-sökningar skiftlägeskänsliga genom att markera alternativet Skiftlägeskänslig sökning i Inställningar / Sökning. Om sökalternativet ”Tecken utan accent matchar tecken med accent och skiljetecken ignoreras” är markerat kommer ett tecken att matcha alla dess varianter (t.ex. ”e” matchar ”é”, ”è”, ”ê” och ”ë”) och alla skiljetecken och blanksteg ignoreras. Till exempel, om alternativet ”Oaccentuerade tecken matchar …” är markerat, får du de två boktitlarna:

1. Big, Bothéred, and Bad

2. Big Bummer

då hittar dessa sökningar:

- title:"er" matches both (’e’ matches both ’é’ and ’e’).

- title:"g " matches both because spaces are ignored.

- title:"g," matches both because the comma is ignored.

- title:"gb" matches both because ’, ’ is ignored in book 1 and spaces are ignored in book 2.

- title:"g b" matches both because comma and space are ignored.

- title:"db" matches #1 because the space in ’and Bad’ is ignored.

- title:"," matches both (it actually matches all books) because commas are ignored.

Om alternativet ”Oaccentuerade tecken matchar …” inte är markerat är teckenvarianter, skiljetecken och mellanrum viktiga.

Du kan bara ställa in ett av sökalternativen ”Skiftlägeskänslig sökning” och ”Oaccentuerade tecken matchar tecken med accent och skiljetecken ignoreras”.

”Jämställdhets”-sökningar

Jämställdhetssökningar indikeras genom att ett likhetstecken (=) sätts in före söksträngen. Till exempel kommer frågan tag:"=science" att matcha science, men inte science fiction eller hard science. Teckenvarianter är signifikanta: é matchar inte e.

Two variants of equality searches are used for hierarchical items (e.g., A.B.C): hierarchical prefix searches and hierarchical component searches. The first, indicated by a single period after the equals (=.) matches the initial parts of a hierarchical item. The second, indicated by two periods after the equals (=..) matches an internal name in the hierarchical item. Examples, using the tag History.Military.WWII as the value:

- tags:"=.Historia" : Sant. ``Historia’’ är ett prefix för taggen.

- tags:"=.History.Military" : True. History.Military is a prefix of the tag.

- tags:"=.History.Military.WWII" : True. History.Military.WWII is a prefix of the tag, albeit an improper one.

- tags:"=.Military" : False. Military is not a prefix of the tag.

- tags:"=.WWII" : False. WWII is not a prefix of the tag.

- tags:"=..History" : True. The hierarchy contains the value History.

- tags:"=..Military" : True. The hierarchy contains the value Military.

- tags:"=..WWII" : True. The hierarchy contains the value WWII.

- tags:"=..Military.WWII" : False. The .. search looks for single values.

Sökningar med ”reguljärt uttryck”

Reguljära uttryckssökningar indikeras genom att prefixet söksträngen läggs till med en tilde (~). Alla Python-kompatibla reguljära uttryck [https://docs.python.org/library/re.html] kan användas. Omvänt snedstreck som används för att undvika specialtecken i reguljära uttryck måste dubbleras eftersom enstaka omvänt snedstreck kommer att tas bort under frågeanalys. Till exempel, för att matcha en bokstavlig parentes måste du ange \\( eller alternativt använda ”supercitattecken” (se nedan). Reguljära uttryckssökningar är ”innehåller”-sökningar om inte uttrycket är förankrat. Teckenvarianter är signifikanta: ~e matchar inte é.

”Teckenvariant”-sökningar

Sökningar efter teckenvarianter indikeras genom att ett insteg (^) sätts in före söksträngen. Denna sökning liknar sökningen ”innehåller” (ovan) förutom att:

- skiftläge ignoreras alltid.

- teckenvarianter matchar alltid varandra.

- skiljetecken och whitespace är alltid betydelsefulla.

Sökalternativen Tecken utan accent matchar tecken med accentuering och skiljetecken ignoreras och Skiftkänslig sökning ignoreras. De har ingen effekt på sökningens beteende.

Följande jämför denna sökning med en innehåller sökning förutsatt att alternativet Oaccentuerade tecken matchar… är markerat (se ovan) med samma två boktitlar:

1. Big, Bothéred, and Bad

2. Big Bummer

sedan hittar dessa teckenvarianter:

- title:"^er" matches both (’e’ matches both ’é’ and ’e’)

- title:"^g" matches both

- title:"^g " matches #2 because the space is significant

- title:"^g," matches #1 because the comma is significant

- title:"^gb" matches nothing because space and comma are significant

- title:"^g b" matches #2 because the comma is significant

- title:"^db" matches nothing

- title:"^," matches #1 (instead of all books) because the comma is significant

Sökuttryckssyntax

Ett ”sökuttryck” är en sekvens av ”söktermer”, valfritt åtskilda av operatorerna ”och” och ”eller”. Om två söktermer förekommer utan en separerande operator, antas och. Operatören och har prioritet över operatorn eller; till exempel är uttrycket a eller b och c samma som a eller (b och c). Du kan använda parentes för att ändra prioritet; till exempel (a eller b) och c för att få eller att utvärdera före and. Du kan använda operatorn not för att negera (invertera) resultatet av att utvärdera ett sökuttryck. Exempel:

- not tag:foo finds all books that don’t contain the tag foo

- not (author:Asimov or author:Weber) finds all books not written by either Asimov or Weber.

The above examples show examples of search terms. A basic search term is a sequence of characters not including spaces, quotes ("), backslashes (\), or parentheses (( )). It can be optionally preceded by a column name specifier: the lookup name of a column followed by a colon (:), for example author:Asimov. If a search term must contain a space then the entire term must be enclosed in quotes, as in title:"The Ring". If the search term must contain quotes then they must be escaped with backslashes. For example, to search for a series named The ”Ball” and The ”Chain”, use:

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

If you need an actual backslash, something that happens frequently in regular expression searches, use two of them (\\).

It is sometimes hard to get all the escapes right so the result is what you want, especially in regular expression and template searches. In these cases use the super-quote: """sequence of characters""". Super-quoted characters are used unchanged: no escape processing is done.

Mer information

To search for a string that begins with an equals, tilde, or caret; prefix the string with a backslash.

Omge söksträngar med citattecken (”) om strängen innehåller parentes eller mellanslag. För att till exempel hitta böcker med taggen Science fiction måste du söka efter tag:"=science fiction". Om du söker för tag:=science fiction hittar du alla böcker med taggen science och ordet fiction i valfri metadata.

Du kan enkelt skapa avancerade sökfrågor med hjälp av den avancerade sökdialogrutan som nås genom att klicka på knappen till vänster om sökrutan.

Tillgängliga fält för sökning är: tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl och anpassade kolumner. Om en enhet är ansluten blir fältet ondevice tillgängligt när du söker i calibre-biblioteksvyn. För att hitta söknamnet (egentligen kallad lookup name) för en anpassad kolumn, håll din muspekare över kolumnrubriken i biblioteksvyn.

Datum

Syntaxen för att söka efter datum är:

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

Om datumet är tvetydigt används den aktuella lokalen för datumjämförelse. Till exempel, i ett språk för mm/dd/åååå tolkas 2/1/2009 som 1 februari 2009. I en dd/mm/åååå språk tolkas det som 2 januari 2009. Vissa speciella datumsträngar är tillgängliga. Strängen idag översätts till dagens datum, oavsett vad det är. Strängarna yesterday och thismonth (eller den översatta motsvarigheten på det aktuella språket) fungerar också. Dessutom kan strängen daysago (även översatt) användas för att jämföra med ett datum för ett antal dagar sedan. Till exempel:

date:>10daysago
date:<=45daysago

För att undvika eventuella problem med översatta strängar när du använder en icke-engelsk version av calibre, är strängarna _today, _yesterday, _thismonth och _daysago alltid tillgängliga. De översätts inte.

Sök efter datum och numeriska värden med relationella jämförelser

Datum och numeriska fält stöder relationsoperatörer = (lika med), > (större än), >= (större än eller lika med), < (mindre än), <= (mindre än eller lika med), and != (inte lika med). Betygsfälten anses vara numeriska. Till exempel sökandet rating:>=3 hittar alla böcker med betyg 3 eller högre.

Du kan söka efter böcker som har ett format av en viss storlek så här:

- size:>1,1M kommer att hitta böcker med ett format som är större än 1,1MB

- storlek:<=1K kommer att hitta böcker med ett format som är mindre än eller lika med 1KB

Du kan söka efter antalet objekt i fält med flera värden, t.ex. taggar med tecknet # och sedan använda samma syntax som numeriska fält. Till exempel, för att hitta alla böcker med fler än 4 taggar använd tags:#>4. Använd tags:#=10 för att hitta alla böcker med exakt 10 taggar.

Serieindex

Serieindex är sökbara. För standardserien är söknamnet series_index. För anpassade seriekolumner, använd kolumnsökningsnamnet följt av _index. Till exempel, för att söka i indexen efter en anpassad seriekolumn med namnet #mina_serier, skulle du använda söknamnet #mina_serie_index. Serieindex är tal, så du kan använda relationsoperatorerna som beskrivs ovan.

Sparade sökningar

Specialfältet search används för sparade sökningar. Om du sparar en sökning med namnet ”Min makes böcker” kan du ange search:"Min makes böcker" i sökfältet för att återanvända den sparade sökningen. Mer om att spara sökningar nedan.

Virtuella bibliotek

Specialfältet vl används för att söka efter böcker som finns i ett virtuellt bibliotek. Till exempel kommer vl:Lästa att hitta alla böckerna i det virtuella biblioteket Lästa. Sökningen vl:Lästa and vl:"Science fiction" hittar alla böcker som finns i båda de virtuella biblioteken Lästa och Science fiction. Värdet som följer vl: måste vara namnet på ett virtuellt bibliotek. Om det virtuella biblioteksnamnet innehåller blanksteg, omge det med citattecken.

Om ett fält har ett värde

Du kan söka efter frånvaron eller närvaron av ett värde för ett fält med hjälp av ”true” och ”false”. Till exempel:

- cover:false finds all books without a cover

- series:true finds all books that are in a series

- series:false finds all books that are not in a series

- comments:false finds all books with an empty comment

- formats:false finds all books with no book files (empty records)

Ja/nej anpassade kolumner

Searching Yes/no custom columns for false, empty, or blank will find all books with undefined values in the column. Searching for true will find all books that do not have undefined values in the column. Searching for yes or checked will find all books with Yes in the column.
Searching for no or unchecked will find all books with No in the column. Note that the words yes, no, blank, empty, checked and unchecked are translated; you can use either the current language’s equivalent word or the English word. The words true and false and the special values _yes, _no, and _empty are not translated.

Identifierare

Identifiers (e.g., ISBN, DOI, LCCN, etc.) use an extended syntax. An identifier has the form type:value, as in isbn:123456789. The extended syntax permits you to specify independently the type and value to search for. Both the type and the value parts of the query can use any of the search kinds. Examples:

- identifiers:true hittar böcker med någon identifierare.

- identifiers:false hittar böcker utan identifierare.

- identifiers:123 kommer att söka efter böcker med vilken typ som helst som har ett värde som innehåller 123.

- identifiers:=123456789 kommer att söka efter böcker av vilken typ som helst som har ett värde som är lika med 123456789.

- identifiers:=isbn: och identifiers:isbn:true hittar böcker med en typ som motsvarar ISBN med något värde

- identifiers:=isbn:false hittar böcker som inte har samma typ som ISBN.

- identifiers:=isbn:123 hittar böcker med en typ som motsvarar ISBN med ett värde som innehåller 123.

- identifiers:=isbn:=123456789 hittar böcker med en typ som motsvarar ISBN med ett värde som är lika med 123456789.

- identifiers:i:1 hittar böcker med en typ som innehåller ett i har ett värde som innehåller en`1`.

Kategorier synliga i taggbläddraren

The search in_tag_browser:true finds all books that are in categories
(tags, authors, etc.) currently shown in the Tag browser. This is useful if you set the two preferences Preferences → Look & feel → Tag browser → Hide empty categories and Find shows all items that match. With those two preferences set, doing a Find in the Tag browser shows only categories containing items matched by the Find. Then, the search in_tag_browser:true additionally finds books with these categories / items. You can easily run this search by pressing the key Ctrl+Alt+Shift+F or clicking the configure button in the Tag browser and choosing the Show only books that have visible categories entry.

Sök med mallar

You can search using a template in Mallspråket för calibre instead of a metadata field. To do so you enter a template, a search type, and the value to search for. The syntax is:

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

The template is any valid calibre template language template. The search type must be one of t (text search), d (date search), n (numeric search), or b (set/not set (boolean)). The value is whatever you want, and can use the search kinds described above for the various search types. You must quote the entire search string if there are spaces anywhere in it.

Exempel:

- template:"program: connected_device_name('main')#@#:t:kindle" – is true when the kindle device is connected.

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" – finds books with EPUB files larger than 1 MB.

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" – finds books with EPUB files newer than 10 days ago.

- template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" – hittar alla böcker som innehåller serienamn i taggarna. Det här exemplet använder supercitationstecken eftersom mallen använder både enkla citattecken (') och dubbla citattecken (") när du konstruerar sökuttrycket.

Du kan enkelt skapa mallsökningsfrågor med hjälp av den avancerade sökdialogrutan som du kommer åt genom att klicka på knappen . Du kan testa mallar på specifika böcker med hjälp av calibre malltestaren, som kan läggas till i verktygsfälten eller menyerna via Inställningar → Verktygsfält och menyer. Den kan också tilldelas en tangentbordsgenväg via Inställningar → Genvägar.

Avancerad sökdialogruta

Avancerad sökdialogruta

Spara sökningar
calibre tillåter dig spara en ofta använd sökning under ett särskilt namn och sedan återanvända den sökningen med ett enda klick. Det gör du genom att skapa din sökning, antingen genom att skriva in det i sökfältet eller använda taggbläddraren. Skriv sedan det namn du vill ge till sökningen i rutan för sparade sökningar bredvid sökfältet. Klicka på plusikonen bredvid rutan för sparade sökningar för att spara sökningen.

Nu kan du komma åt din sparade sökning i taggbläddraren under Sparade sökningar. Ett enda klick gör att du kan återanvända en godtyckligt komplex sökning enkelt, utan att behöva återskapa den.

Sökning i hela texten i alla böcker
calibre kan valfritt indexera hela texten för böcker i biblioteket för att möjliggöra omedelbar sökning av ord i vilken bok som helst. För att använda den här funktionen klickar du på knappen FT till vänster i sökfältet.

Aktivera sedan indexering för det aktuella biblioteket. När indexeringen är klar kan du söka i all text i hela biblioteket. När du lägger till nya böcker i biblioteket indexeras de automatiskt i bakgrunden. Denna sökning låter dig snabbt hitta alla böcker som innehåller ett ord eller en kombination av ord. Du kan till och med söka efter ord som förekommer nära andra ord, som visas i exemplen i sökpopupfönstret. Observera att det här sökverktyget endast hittar en förekomst av sökfrågan i en viss bok, inte listar varje förekomst, för det är det bäst att söka inuti boken med calibre e-bokvisaren.

Du kan återindexera en enskild bok genom att högerklicka på bokdetaljpanelen i calibre och välja Reindexera den här boken för fulltextsökning.

Virtuella bibliotek

Ett virtuellt bibliotek är ett sätt att låtsas att ditt calibre-bibliotek bara har ett fåtal böcker istället för dess fullständiga samling. Detta är ett utmärkt sätt att partitionera din stora samling av böcker i mindre, hanterbara bitar. Om du vill veta hur du skapar och använder virtuella bibliotek, se handledningen: Virtuella bibliotek.

Tillfällig markering av böcker

Du kan tillfälligt markera godtyckliga uppsättningar böcker. Markerade böcker kommer att ha en nål på sig och kan hittas med sökningen marked:true. För att markera en bok, tryck på Ctrl+M eller gå till Inställningar → Verktygsfält och menyer och lägg till knappen Markera böcker i huvudverktygsfältet.

Du kan markera böcker med en specifik textetikett genom att högerklicka på knappen Markera böcker och välja Markera böcker med textetikett. Böcker markerade med textetiketter kan senare hittas med sökningen marked:"=texten-du-angav".

Uppskatta metadata från filnamn

Normalt läser calibre metadata från bokfilen. Emellertid kan den anpassas för att läsa metadata från filnamnet istället, via Inställningar → Importera/exportera → Lägga till böcker → Läs metadata från filinnehåll istället för filnamn.

Du kan också anpassa hur metadata läses från filnamnet med reguljära uttryck (se Allt om att använda reguljära uttryck i calibre). I Lägga till böcker avsnittet av anpassningsdialogrutan kan du ange ett reguljärt uttryck som calibre kommer att använda för att försöka uppskatta metadata från namnen på e-bokfiler som du lägger till i biblioteket. Standard för reguljära uttrycket är:

title - author

det vill säga, det antas att alla tecken fram till den första - är titeln på boken och efterföljande tecken är författare till boken. Till exempel filnamnet:

Foundation and Earth - Isaac Asimov.txt

kommer att tolkas till att ha titeln: Foundation and Earth och författare: Isaac Asimov

Tips

Om filnamnet inte innehåller bindestrecket kommer ovanstående reguljära uttryck att misslyckas.

Bokdetaljer

Bokdetaljer visar omslag och alla metadata för den för närvarande valda boken. Det kan döljas med knappen Layout i det nedre högra hörnet av calibre-huvudfönstret. De författarnamn som visas i bokdetaljpanelen är klickbara, de kommer som standard att ta dig till Wikipedia-sidan för författaren. Detta kan anpassas genom att högerklicka på författarens namn och välja Hantera denna författare.

Likaså om du hämtar metadata för boken, visar bokdetaljpanelen automatiskt dig länkar som hänvisar till webbplatser för boken på Amazon, WorldCat osv. från där metadata hämtades.

Du kan högerklicka på enskilda e-bokformat i bokdetaljpanelen för att ta bort dem, jämföra dem med sina originalversioner, spara dem till disk, öppna dem med ett externt program osv.

Du kan byta omslaget på boken genom att helt enkelt dra och släppa en bild på bokdetaljpanelen. Om du vill redigera omslagsbilden i ett externt program, högerklickar du helt enkelt på den och väljer Öppna med.

Du kan också lägga till e-boksfiler till den aktuella boken genom att dra och släppa filerna till bokdetaljpanelen.

Dubbelklicka på bokdetaljpanelen så öppnas den i ett separat fönster.

Slutligen kan du skräddarsy exakt vilken information som visas i bokdetaljpanelen via Inställningar → Gränssnitt → Utseende & känsla>Bokdetaljer.

Taggbläddrare

Taggbläddraren tillåter dig enkelt bläddra i din samling författare/taggar/serier/osv. Om du klickar på en post i taggbläddraren, till exempel författarens namn Isaac Asimov, då begränsas listan med böcker till höger till att visa böcker av denna författare. Du kan klicka på kategorinamn också. Till exempel klicka på ”Serier” kommer att visa dig alla böcker i någon serie.

Det första klicket på en post kommer att begränsa listan med böcker till de som innehåller eller matchar posten. Om du fortsätter med ovanstående exempel klickar du på Isaac Asimov för att visa böcker av den författaren. Genom att klicka igen på objektet ändras det som visas beroende på om objektet har underkategorier (se underkategorier och hierarkiska artiklar nedan). Om du fortsätter med exemplet Asimov, klickar du igen på Isaac Asimov och begränsar listan över böcker till dem som inte är av Isaac Asimov. Ett tredje klick tar bort begränsningen, visar alla böcker. Om du håller ned tangenterna Ctrl eller Skift och klickar på flera poster, skapas begränsningar baserade på flera poster. Till exempel kan du hålla Ctrl och klicka på taggarna Historia och Europa för att hitta böcker om Europas historia. Taggbläddraren fungerar genom att bygga sökuttryck som automatiskt matas in i sökfältet. Att se vad taggbläddraren skapar är ett bra sätt att lära sig hur man konstruerar grundläggande sökuttryck.

Poster i taggbläddraren har sina ikoner delvis färgade. Mängden färg beror på medelbetyget av böckerna i den kategorin. Så till exempel om böcker av Isaac Asimov har i genomsnitt fyra stjärnor, visas ikonen för Isaac Asimov i taggbläddraren vara 4/5:e färgad. Du kan hålla din mus över ikonen för att se det medelbetyget.

Posten i yttre nivå av taggbläddraren, som författare och serier, kallas kategorier. Du kan skapa egna kategorier, så kallade användarkategorier, som är användbara för att organisera poster. Du kan till exempel använda användarkategoriredigeraren (klicka på knappen Anpassa längst ner till vänster av taggbläddraren och välj Författare, taggar osv. → Användarkategorier) för att skapa en användarkategori som kallas Favoritförfattare och placera sedan posterna för dina favoriter i kategorin. Användarkategorier kan ha underkategorier. Till exempel är användarkategorin Favoriter.Författare en underkategori av Favoriter. Du kanske också ha Favoriter.Serier, i vilket fall kommer det kommer att finnas två underkategorier under Favoriter. Underkategorier kan skapas genom att högerklicka på en användarkategori och att välja Lägg till underkategori i… och ange underkategorinamnet; eller genom att använda Redigering av användarkategorier genom att ange namn som exempel Favoriter i exemplet ovan.

Du kan söka användarkategorier på samma sätt som inbyggda kategorier, genom att klicka på dem. Det finns fyra olika sökningar som cyklas igenom genom att klicka:

1. ”allt som matchar en post i kategorin” anges med ett enda gröna plustecknet.

2. ”allt som matchar en post i den kategori eller dess underkategorier” anges med två gröna plustecknet.

3. ”allt som inte matchar en post i kategorin” visas med ett enda röd minustecken.

4. ”allt som inte matchar en post i den kategori eller dess underkategorier” visas av två röda minustecken.

Det är också möjligt att skapa hierarkier inom några av kategorierna text såsom taggar, serier och anpassade kolumner. Dessa hierarkier visar den lilla triangeln, medger delposter att vara dolda. För att använda hierarkier av post i en kategori, måste du först gå till Inställningar → Gränssnitt → Utseende och känsla och ange kategorinamnet eller kategorinamnen i fältet ”Kategorier med hierarkisk post”. När detta är gjort, kommer poster i den kategorin som innehåller punkter visas med hjälp av den lilla triangeln. Till exempel anta att du skapar en egen kolumn som heter ”genre” och hänvisar till att den innehåller hierarkiska post. När detta är gjort, sådant som mysterium.rysare och mysterium.engelska visas som mysterium med den lilla triangeln bredvid den. Genom att klicka på triangeln visas rysare och engelska som delposter. Se Hantera undergrupper av böcker, till exempel ”genre” för mer information.

Hierarkiska post (post med underkategorier) använder samma fyra ”klicka-på” sökningar som användarkategorier. Poster som inte har skapelser använder två av de sökningar: ”allt som matchar” och ”allt som inte matchar”.

Du kan dra och släppa poster i taggbläddraren på användarkategorier för att lägga till dem i den kategorin. Om källan är en användarkategori, flyttas posten till den nya kategorin om du håller ner Skift-tangenten medan du drar. Du kan också dra och släppa böcker från boklistan på posten i taggbläddraren; Om du släpper en bok på en post kommer posten att automatiskt tillämpas på de släppta böckerna. Om du till exempel drar en bok på Isaac Asimov kommer författaren till den boken att bli Isaac Asimov. Om du släpper den på taggen Historik läggs taggen Historik till i bokens taggar.

Du kan enkelt hitta vilket objekt som helst i taggbläddraren genom att klicka på sökknappen i det nedre högra hörnet. Dessutom kan du högerklicka på valfritt objekt och välja en av flera operationer. Några exempel är att dölja det, byta namn på det eller öppna en ”Hantera x”-dialogruta som låter dig hantera objekt av det slaget. Till exempel, Hantera författare-dialogrutan låter dig byta namn på författare och kontrollera hur deras namn sorteras.

Du kan anpassa hur post sorteras i taggbläddraren via knappen Anpassa längst ned till vänster på taggbläddraren. Du kan välja att sortera efter namn, medelbetyg eller popularitet (popularitet är antalet böcker med en post i ditt bibliotek, till exempel är populariteten av Isaac Asimov antalet böcker i ditt bibliotek av Isaac Asimov).

Omslagsrutnät

Du kan låta calibre visa ett rutnät av bokomslag stället för en lista med böcker, om du föredrar att bläddra i din samling av omslag istället. Omslagsrutnätet aktiveras genom att klicka på knappen Layout i det nedre högra hörnet av calibre-huvudfönstret. Du kan anpassa omslagsstorlekar och bakgrunden till Omslagrutsnät via Inställningar → Gränssnitt → Utseende & känsla>Omslagrutsnät. Du kan även låta calibre visa någon angivna fältet under omslaget, t.ex. titel eller författare eller betyg eller en egen kolumn som du själva har utmatningsformat.

Omslagsbläddrare

Förutom omslagsrutnätet som beskrivs ovan, kan du också låta calibre visa omslag i en enskild rad. Detta aktiveras via knappen Layout i det nedre högra hörnet av huvudfönstret. I Inställningar → Gränssnitt → Utseende & känsla → Omslagsbläddrare kan du ändra antalet omslag som visas, och även välja att visa omslagsbläddraren i ett separat fönster.

Adding notes for authors, series, etc.

You can add notes for an author/series/tag/publisher/etc. to your calibre library. To do so right click on the author name in the Tag browser on the left or the Book details panel on the right and choose Create note or Edit note.

A simple popup window will allow you to enter your notes using basic formatting and supporting links and images. Once a note for an author is created, it can be viewed easily from the Book details panel by clicking the little pencil icon next to the author name.

You can search through all the notes in your library using the Browse notes tool by pressing Ctrl+Shift+N or adding it to the toolbar via Preferences → Toolbars & menus.

Snabbvisning

Ibland vill du välja en bok och snabbt få en lista med böcker med samma värde i någon kategori (författare, taggar, utgivare, serier osv.) som den bok som för närvarande är vald, men utan att ändra den aktuella vyn i biblioteket. Du kan göra detta med snabbvisning. Snabbvisning öppnar antingen ett andra fönster eller en panel i boklistan som visar listan över böcker som matchar värdet av intresse. Antag till exempel att du vill se en lista över alla böcker med en eller flera av författarna till den valda boken. Klicka i den författarcell du är intresserad av och tryck på tangenten ”Q” eller klicka på Snabbvisnings-ikonen i Layout-alternativen för calibre-fönstret. Ett fönster eller en panel öppnas med alla författare för den boken till vänster och alla böcker av den valda författaren till höger.

Några exempel på snabbvisningsanvändningar: snabbt se vilka andra böcker:

- har en eller flera taggar tillämpade på den för närvarande valda boken,

- finns i samma serier som den aktuella boken

- har samma värden i en anpassad kolumn som den aktuella boken

- är skrivna av en av samma författare till den aktuella boken

- dela värden i en anpassad kolumn

Det finns två alternativ för var snabbvisningsinformationen visas:

1. Det kan öppnas ”inte förankrad”: ovanpå calibre-fönstret och kommer att vara öppet tills du uttryckligen stänger den.

2. Den kan öppnas ”förankrad”: som en panel i boklistan av calibre-huvudfönstret.

Du kan flytta fönstret från förankrad till inte förankrad som önskat med knappen ”Förankra/Förankra inte”.

Snabbvisningspanelen kan lämnas öppen permanent, i vilket fall den följer rörelser i boklistan. Om du till exempel klickar i calibre-biblioteksvyn på en kategorikolumn (taggar, serier, utgivare, författare osv.) för en bok ändras innehållet i snabbvisningsfönstret för att visa värdena för den kategorin på vänster sida för den valda boken (t.ex. taggarna för den boken). Den första posten i listan kommer att väljas och snabbvisning visar dig på den högra sidopanelen alla böcker i ditt bibliotek som använder det värdet. Klicka på ett annat värde i den vänstra panelen för att se böckerna med värdet.

Dubbelklicka på en bok i snabbvisningsfönstret för att välja den bok i biblioteksvyn. Detta kommer också ändra post som visas i snabbvisningsfönstret (det vänstra panelen) för att visa posterna i den nyligen valda boken.

Skift- or Ctrl-dubbelklicka på en bok i snabbvisningsfönstret för att öppna dialogrutan för redigering av metadata för den boken i calibre-fönstret. Den redigerade boken kommer att visas i snabbvisningspanelen när du stänger dialogrutan för redigering av metadata.

Du kan se om en kolumn kan snabbvisas genom att hålla din mus över kolumntiteln och tittar på verktygstipset för denna titel. Du kan också veta genom att högerklicka på kolumn titeln för att se om ”Snabbvisning”-alternativet visas i menyn, i vilket fall välja att snabbvisningsalternativet motsvarar att trycka ”Q” i den aktuella cellen.

Alternativ (i Inställningar → Utseende & känsla → Snabbvisning):

- Respektera (eller inte) det aktuella virtuella biblioteket. Om markerad visar snabbvisning endast böcker i det aktuella virtuella biblioteket. Standard: respektera virtuella bibliotek

- Ändra innehållet i snabbvisningsfönstret när kolumnen ändras på boklistan med markörknapparna. Standard: Följ inte ändringar gjorda med markörknappar

- Ändra kolumnen som snabbvisas när en cell i snabbvisningsfönstret dubbelklickas. Annars ändras boken men inte den kolumn som undersöks. Standard: ändra kolumnen

- Ändra kolumnen som snabbvisas till den aktuella kolumnen när du trycker på returtangenten i snabbvisningspanelen. Annars ändras boken men den kolumn som undersöks är inte. Standard: ändra kolumnen

- Välj vilka kolumner som ska visas i snabbvisningsfönstret/panelen.

Jobb

Jobb-panelen visar antalet jobb som är pågående. Jobb är uppgifter som körs i en separat process. De omfattar konvertera e-böcker och prata med din läsenhet. Du kan klicka på jobbpanelen för att komma åt listan med jobb. När ett jobb har slutförts kan du se en detaljerad logg från det jobbet genom att dubbelklicka på den i listan. Detta är användbart för att felsöka jobb som kanske inte har slutförts.

Tangentbordsgenvägar
calibre has several keyboard shortcuts to save you time and mouse movement. These shortcuts are active in the book list view (when you’re not editing the details of a particular book), and most of them affect the title you have selected. The calibre E-book viewer has its own shortcuts which can be customised in the viewer Preferences.

Observera

Observera: Tangentbordsgenvägarna i calibre kräver ingen modifieringstangent (kommando, alternativ, kontroll osv.) såvida inte detta särskilt anges. Du behöver bara trycka på bokstavsknappen, t.ex. E för att redigera.

Tangentbordsgenvägar för calibre-huvudprogrammet

Tangentbordsgenväg

Åtgärd

F2 (Enter på macOS)

Redigera metadata för det valda fältet i boklistan.

A

Lägg till böcker

Skift+A

Lägg till format i de valda böckerna

C

Konvertera valda böcker

D

Överför till enhet

Del

Ta bort valda böcker

E

Redigera metadata av valda böcker

G

Skaffa böcker

I

Visa bokdetaljer

K

Redigera innehållsförteckning

M

Slå samman valda poster

Alt+M

Slå samman valda poster, behåll originalen

O

Öppna innehållande mapp

P

Polera böcker

S

Spara till disk

T

Redigera bok

V

Visa

Skift+V

Visa senast läst bok

Alt+V/Cmd+V på macOS

Visa specifikt format

Alt+Skift+J

Växla jobblista

Alt+Skift+B

Växla omslagsbläddraren

Alt+Skift+D

Växla bokdetaljpanelen

Alt+Skift+T

Växla taggbläddraren

Alt+Skift+G

Växla omslagrutnästvyn

Alt+A

Visa böcker av samma författare som aktuell bok

Alt+T

Visa böcker med samma taggar som aktuell bok

Alt+P

Visa böcker av samma utgivare som aktuell bok

Alt+Skift+S

Visa böcker i samma serie som aktuell bok

/, Ctrl+F

Fokusera på sökfältet

Skift+Ctrl+F

Öppna den avancerade sökdialogrutan

Skift+Alt+F

Växla sökfältet

Esc

Rensa den aktuella sökningen

Skift+Esc

Fokusera på boklistan

Ctrl+Esc

Rensa det virtuella biblioteket

Alt+Esc

Rensa kompletterande begränsningen

Ctrl+*

Skapa ett temporärt virtuellt bibliotek baserat på den aktuella sökningen

Ctrl+Höger

Välj nästa flik för virtuella bibliotek

Ctrl+Vänster

Välj föregående flik för virtuella bibliotek

N eller F3

Hitta nästa bok som matchar den aktuella sökningen (fungerar bara om sökmarkering är aktiverad i sökalterativen)

Skift+N eller Skift+F3

Hitta den föregående boken som matchar den aktuella sökningen (fungerar bara om sökmarkering är aktiverad i sökinställningarna)

Ctrl+D

Hämta metadata och omslag

Ctrl+R

Starta om calibre

Ctrl+Skift+R

Starta om calibre i felsökningsläge

Skift+Ctrl+E

Lägg till tomma böcker i calibre

Ctrl+M

Växla markeringsstatus på valda böcker

Ctrl+/ eller Ctrl+Alt+F

Öppna popupfönstret för att söka i hela texten för alla böcker i biblioteket

Q

Öppna snabbsökningsfönstret för visa böcker med relaterade serier/taggar/osv.

Skift+Q

Fokusera på den öppnade snabbsökningspanelen

Skift+S

Utför en sökning i snabbsökningspanelen

F5

Tillämpa den aktuella sorteringen igen

Ctrl+Q

Avsluta calibre

X

Växla automatisk rullning av boklistan

Ctrl+Alt+Skift+F

Begränsa de visade böckerna till endast de böcker som finns i en kategori som för närvarande visas i Taggbläddraren

B

Browse annotations (highlights and bookmarks) made in the calibre viewer for all books in the library

Ctrl+Skift+N

Browse notes associated with authors/series/tags/etc.

Alt+Shift+L

Toggle the layout between wide and narrow views

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Lägga till din favorit nyhetswebbplats

Lägga till din favorit nyhetswebbplats
calibre har ett kraftfullt, flexibelt och lättanvänt ramverk för att hämta nyheter från Internet och konvertera dem till en e-bok. Följande kommer att visa dig, med hjälp av exempel, hur du får nyheter från olika webbplatser.

För att få en förståelse för hur man använder ramverket, följ exemplen i den ordning som anges nedan:

- Helt automatisk hämtning

- calibre-bloggen

- bbc.co.uk

- Anpassa hämtningsprocessen

- Använda den tryckta versionen av bbc.co.uk

- Ersätta artikelformat

- Skivning och styckning i tärningar

- Exempel i verkligheten

- Tips för att utveckla nya recept

- Ytterligare läsning

- API-dokumentation

Helt automatisk hämtning

Om din nyhetskälla är enkel nog, skulle calibre mycket väl kunna hämta det helt automatiskt, allt du behöver göra att ge URL:en. calibre samlar all information som behövs för att hämtar en nyhetskälla i ett recept. För att berätta för calibre om en nyhetskälla, måste du skapa ett recept för det. Låt oss se några exempel:

calibre-bloggen
calibre-bloggen är en blogg med inlägg som beskriver många användbara calibre-funktioner på ett enkelt och lättillgängligt sätt för nya calibre-användare. För att hämtar den här bloggen till en e-bok förlitar vi oss på RSS flödet av bloggen:

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

Jag fick RSS-URL:en genom att titta under ”Prenumerera” längst ner på bloggsidan och välja Posts → Atom. För att få calibre att hämtar flöden och konvertera dem till en e-bok, bör du högerklicka på knappen Hämta nyheter och sedan på menyalternativet Lägg till en anpassad nyhetskälla och sedan knappen Nytt recept. En dialogruta liknande den som visas nedan bör öppnas.

Ange först Calibre Blog i fältet Recepttitel. Detta kommer att vara titeln på e-boken som kommer att skapas från artiklarna i ovanstående flöden.

De nästa två fälten (Äldsta artikel och Högst antal artiklar) tillåter dig viss kontroll över hur många artiklar som ska hämtats från varje flöde, och de är ganska självförklarande.

För att lägga till flöden till receptet, mata in flödestitel och flödes-URL:en och klicka på knappen Lägg till flöde. När du har lagt till flödet klickar du på knappen Spara och du är klar! Stäng dialogrutan.

Så här testar du nya recept klicka på knappen Hämta nyheter och i undermenyn Anpassad nyhetskälla klicka på calibre Blog. Efter ett par minuter, kommer den nyligen hämtade e-boken av blogginlägg visas i huvudbiblioteksvyn (om du har din läsenhet ansluten kommer den att läggas till på läsenheten istället för i biblioteket). Välj det och tryck på knappen Visa för att läsa!

Anledningen till att detta fungerat så bra, med så liten ansträngning är att bloggen ger fullt innehåll RSS flöden, det vill säga, artikelns innehåll är inbäddat i själva flödet. För de flesta nyhetskällor som tillhandahåller nyheter på detta sätt, med fullt innehåll flöden, du behöver inte någon mer ansträngning för att konvertera dem till e-böcker. Nu ska vi titta på en nyhetskälla som inte ger fullständigt innehållsflöde. I sådana flöden är hela artikeln en webbplats och flödet innehåller bara en länk till webbplatsen med en kort sammanfattning av artikeln.

bbc.co.uk

Låt oss testa följande två flöden från BBC:

1. Nyheter framsida: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml

2. Vetenskap/Natur: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml

Följ proceduren som beskrivs i calibre-bloggen ovan för att skapa ett recept på BBC (med hjälp av flöde ovan). Om man tittar på den hämtade e-boken, ser vi att claibre har gjort ett förtjänstfullt arbete för att utvinna bara innehållet som du bryr dig om från varje artikel webbplats. Emellertid är extraktionsprocessen inte perfekt. Ibland lämnar oönskade innehåll som menyer och navigeringshjälpmedel, eller det tar bort innehåll som borde lämnas i fred, som artikelrubriker. För att få perfekt innehållsutvinning, måste vi anpassa hämta processen, som beskrivs i nästa avsnitt.

Anpassa hämtningsprocessen

När du vill att fullända hämtningsprocessen eller hämtar innehåll från en särskilt komplex webbplats kan du utnyttja hela kraften och flexibiliteten i ramverket recept. För att göra det, klicka på knappen Växla till avancerat läge i dialogrutan Lägg till anpassade nyhetskällor.

Den enklaste och ofta produktivaste anpassningen är att använda utskriftsversioner av artiklar på nätet. Utskriftsversionen har vanligtvis mycket mindre skräp och översätter mycket smidigare till en e-bok. Låt oss försöka använda utskriftsversionen av artiklarna från BBC.

Använda den tryckta versionen av bbc.co.uk

Det första steget är att titta på e-boken som vi hämtat tidigare från bbc.co.uk. Vid slutet av varje artikel, i e-boken finns en liten baksidetext som talar om när artikeln hämtats från.
Kopiera och klistra in denna URL:en i en webbläsare.
Nu på artikelwebbplatsen leta efter en länk som hänvisar till ”Utskriftbar version”. Klicka på den för att se den tryckta versionen av artikeln. Det ser mycket snyggare! Jämför nu de två webbadresser. För mig var de:

Artikelns URL

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

Utskriftsversionens URL

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

För att det ska se ut som att få den tryckta versionen, måste vi prefixa varje artikel-URL med:

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

Nu i Avancerat läge i dialogrutan Anpassade nyhetskällor ska du se något liknande (kom ihåg att välja BBC-receptet innan du växlar till avancerat läge):

Du kan se att fälten från Grundläge har omräknats med Python-kod på ett enkelt sätt. Vi måste lägga till instruktioner för att detta recept för att använda den tryckta versionen av artiklarna. Allt som behövs är att lägga till följande två rader:

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

Detta är Python, så indrag är viktigt. När du har lagt till raderna ska det se ut som:

I ovanstående, def print_version(self, url) definieras en metod som anropas av calibre för varje artikel. url är webbadressen till den ursprungliga artikeln. Vad print_version gör är att den tar webbadressen och ersätter den med den nya webbadressen som hänvisar till den tryckta versionen av artikeln. Om du vill veta om Python [https://www.python.org] se tutorial [https://docs.python.org/tutorial/].

Nu klickar du på knappen Lägg till/uppdatera recept och dina ändringar sparas. Hämta e-boken igen. Du borde nu ha en mycket förbättrad e-bok. Ett av problemen med den nya versionen är att teckensnitten på utskriftsversionens webbplats är för små. Det här är automatiskt åtgärdat vid konvertering till en e-bok, men även efter justeringsprocessen blir teckensnittsstorleken på menyer och navigeringsfältet för stor i förhållande till artikeltexten. För att åtgärda det här, kommer vi att göra lite mer anpassning, i nästa avsnitt.

Ersätta artikelformat

I föregående avsnitt såg vi att teckensnittsstorleken efter artiklar från den tryckta versionen av BBC var för liten. I de flesta webbplatser, ingår BBC, denna teckensnittsstorlek är inställd med hjälp av CSS-formatmallar. Vi kan inaktivera hämtning av sådana formatmallar genom att lägga till raden:

no_stylesheets = True

Receptet ser nu ut så här:

Den nya versionen ser ganska bra ut. Om du är en perfektionist, vill du nog läsa nästa avsnitt, som handlar faktiskt om att modifiera hämtat material.

Skivning och styckning i tärningar
calibre innehåller mycket kraftfulla och flexibla förmågor när det gäller att manipulera hämtat innehåll. För att visa upp ett par av dessa, låt oss titta på vår gamle vän receptet BBC igen. Om man tittar på källkoden (HTML) i ett par artiklar (utskriftsversion) ser vi att de har en sidfot som inte innehåller någon användbar information, som finns i

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

Detta kan tas bort genom att lägga till:

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

till receptet. Slutligen, låt oss ersätta några av CSS som vi inaktiverade tidigare med vår egen CSS som är lämplig för konvertering till en e-bok:

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

Med dessa tillägg har vårt recept blivit ”produktionskvalitet”.

Detta recept utforskar bara toppen av isberget när det kommer till makten av calibre. För att utforska mer av förmågan hos calibre så undersöker vi en mer komplex verkliga livet exempel i nästa avsnitt.

Exempel i verkligheten

Ett tämligen komplicerat exempel från verkligheten som exponerar mer av API för BasicNewsRecipe är detta recept för 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'))

Vi ser flera nya funktioner i detta recept. Först har vi:

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

Detta ställer den visade tiden på förstasidan av den skapade e-boken att vara i formatet, Dag, Dag_Nummer Månad, År. Se timefmt.

Då ser vi en grupp direktiv för att rensa hämtad HTML:

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

Dessa tar bort allt innan den första <h1>-taggen och allt efter den första taggen vars id är footer. Se remove_tags, remove_tags_before, remove_tags_after.

Nästa intressanta funktion är:

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

needs_subscription = True berättar för calibre att detta recept behöver användarnamn och lösenord för att komma åt innehållet. Detta medför att calibre frågar efter användarnamn och lösenord när du försöker använda det här receptet. Koden i calibre.web.feeds.news.BasicNewsRecipe.get_browser() loggar faktiskt in på NYT-webbplatsen. När du loggat in kommer calibre att använda samma webbläsarinstans för att hämta allt innehåll. Se mechanize [https://mechanize.readthedocs.io/en/latest/] för att förstå koden i get_browser.

Nästa nya funktion är calibre.web.feeds.news.BasicNewsRecipe.parse_index() metoden. Dess uppgift är att gå till https://www.nytimes.com/pages/todayspaper/index.html och hämta listan med artiklar som visas i dagens tidning. Medan mer komplex än att bara använda RSS, skapar receptet en e-bok som motsvarar mycket nära dagens tidning. parse_index använder mycket BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] för att analysera dagens tidningswebbplats. Du kan också använda andra, mer moderna parsers om du ogillar
BeautifulSoup. calibre comes with lxml [https://lxml.de/] och html5lib [https://github.com/html5lib/html5lib-python], vilka är de rekommenderade parsers. För att använda dem, ersätt anropet till index_to_soup() med följande:

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)

Den sista nya funktionen är metoden calibre.web.feeds.news.BasicNewsRecipe.preprocess_html(). Den kan användas för att utföra godtyckliga omvandlingar på varje hämtad HTML-sida. Här används den för att kringgå de annonser som de nytimes visar dig före varje artikel.

Tips för att utveckla nya recept

Det bästa sättet att utveckla nya recept är att använda kommandoradsgränssnittet. Skapa receptet med din favorit Python-redigerare och spara det i en fil, t.ex. myrecipe.recipe. Filändelsen .recipe krävs. Du kan hämtar innehållet med det här receptet med kommandot:

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

Kommandot ebook-convert kommer att hämtar alla webbplatser och spara dem i EPUB-filen myrecipe.epub. Flaggan -vv får ebook-convert att mata ut mycket information om vad den gör. Flaggan ebook-convert-recipe-input --test begränsar hämtningen till bara ett par artiklar från högst två flöden. Dessutom placerar ebook-convert HTML-filen i mappen debug/input`, där ``debug är mappen du angav i flaggan ebook-convert --debug-pipeline.

När hämtningen är klar kan du titta på hämtad HTML genom att öppna filen debug/input/index.html i en webbläsare. När du är nöjd med att hämtningen och förbehandling sker korrekt kan du skapa e-böcker i olika format enligt nedan:

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

Om du är nöjd med ditt recept och du känner att det finns tillräcklig efterfrågan för att motivera dess införande i uppsättningen av inbyggda recept, lägg upp ditt recept i calibres receptforum [https://www.mobileread.com/forums/forumdisplay.php?f=228] för att dela med andra calibre-användare.

Observera

På macOS finns kommandoradsverktygen inuti calibre paketet, till exempel om du installerade calibre i /Applications kommandoradsverktygen finns i /Applications/calibre.app/Contents/MacOS/.

Se även

ebook-convert

Kommandoradsgränssnittet för all e-bokkonvertering.

Ytterligare läsning

Om du vill veta mer om att skriva avancerade recept med några av faciliteterna som finns i BasicNewsRecipe bör du konsultera följande källor:

API-dokumentation

Dokumentation av BasicNewsRecipe-klassen och alla dess viktiga metoder och fält.

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

Källkoden för BasicNewsRecipe

Inbyggda recept [https://github.com/kovidgoyal/calibre/tree/master/recipes]

Källkoden för de inbyggda recept som kommer med calibre

calibre-receptforumet [https://www.mobileread.com/forums/forumdisplay.php?f=228]

Här finns massor av kunniga receptförfattare för calibre.

API-dokumentation

API-dokumentation för receptBasicNewsRecipe

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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Lägga till din favorit nyhetswebbplats »
- API-dokumentation för recept

API-dokumentation för recept

API:et för skriva recept är definierade av BasicNewsRecipe
class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[source]

Grundklass som innehåller logik behövs i alla recept. Genom att åsidosätta gradvis mer av funktionaliteten i den här klassen kan du göra allt mer anpassade/kraftfulla recept. För en handledningsintroduktion till att skapa recept, se Lägga till din favorit nyhetswebbplats.

abort_article(msg=None)[source]

Anropa denna metod inuti någon av förbehandlingsmetoderna för att avbryta hämtningen av den aktuella artikeln. Användbart att hoppa över artiklar som innehåller olämpligt innehåll, såsom hela videoklippsartiklar.

abort_recipe_processing(msg)[source]

Orsakar att recepthämtningssystemet avbryter hämtningen av detta recept, visning av ett enkelt återkopplingsmeddelande till användaren.

add_toc_thumbnail(article, src)[source]

Anropa detta från populate_article_metadata med attributet src för en <img>-tagg från artikeln som är lämplig att använda som miniatyrbild som representerar artikeln i innehållsförteckningen. Huruvida miniatyrbilden faktiskt används är enhetsberoende (används för närvarande endast av Kindle-läsenheter). Observera att den refererade bilden måste vara en som hämtats ner, annars kommer den att ignoreras.

classmethod adeify_images(soup)[source]

Om ditt recept vid konvertering till EPUB har problem med bilderna när de visas i Adobe Digital Editions, anropa den här metoden inifrån postprocess_html().

canonicalize_internal_url(url, is_link=True)[source]

Returnera en uppsättning kanoniska representationer av url. Standardimplementeringen använder bara serverns värdnamn och sökväg för URL:en, utan att ignorerar eventuella frågeparametrar, fragment osv. De kanoniska representationerna måste vara unika för alla URL:er för den här nyhetskällan. Om de inte är det kan interna länkar lösas felaktigt.

Parametrar:

is_link – Är True om URL:en kommer från en intern länk i en HTML-fil. False om URL:en är URL:en som används för att hämta en artikel.

cleanup()[source]

Anropas efter alla artiklar har hämtats. Använd den för att göra godtycklig sanering som att logga ut från prenumerationsplatser osv.

clone_browser(br)[source]

Klona webbläsaren br. Klonade webbläsare används för flertrådade hämtningar, eftersom mechanize (mekanisera) inte är trådsäkert. Standard kloningsrutinerna ska fånga de flesta webbläsaranpassningar, men om du gör något exotiskt i ditt recept, ska du åsidosätta den här metoden i ditt recept och klona manuellt.

Klonade webbläsarinstanser använder samma, trådsäkra CookieJar som standard, såvida du inte har anpassad hantering av kakor.

default_cover(cover_file)[source]

Skapa ett generiskt omslag för recept som inte har ett omslag

download()[source]

Hämta och förbearbeta alla artiklar från flödena i detta recept. Denna metod bör endast anropas en gång för en viss receptinstans. Att anropa den mer än en gång kommer att leda till odefinierat beteende. :return: Sökväg till index.html

extract_readable_article(html, url)[source]

Extraherar huvudartikelinnehållet från ’html’, rensas upp och returneras som en (artikel_html, extraherad_titel) tupel. Baserat på den ursprungliga läsbarhetsalgoritmen från Arc90.

get_article_url(article)[source]

Åsidosätt i en underklass för att anpassa extraheringen av URL som hänvisar till innehållet för varje artikel. Returnera artikelwebbadressen. Den anropas med article, ett objekt som representerar en analyserad artikel från ett flöde. Se feedparser [https://pythonhosted.org/feedparser/]. Normalt söker den efter den ursprungliga länken (till flöde syndikerade via en tjänst som FeedBurner eller Pheedo) och om den hittas, returnerar detta eller annars returneras article.link [https://pythonhosted.org/feedparser/reference-entry-link.html].

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

Returnera en webbläsarinstans som används för att hämta dokument från webben. Som standard returneras en mechanize (mekanisera) [https://mechanize.readthedocs.io/en/latest/] webbläsarinstans som stöder kakor, ignorerar robots.txt, hanterar uppdateringar och har en Mozilla Firefox-användaragent.

Om ditt recept kräver att du loggar in först, åsidosätt denna metod i din underklass. Till exempel används följande kod i receptet New York Times för att logga in för full åtkomst:

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]

Returnera en URL till omslagsbild för den här utgåvan eller None. Som standard returneras värdet av elementet self.cover_url som normalt är None. Om du vill att ditt recept för att hämtar ett omslag för e-boken åsidosätter den här metoden i din underklass, eller ställa medlemsvariabeln self.cover_url innan denna metod anropas.

get_extra_css()[source]

Som standard returneras self.extra_css. Åsidosätt om du vill programmässigt skapa extra_css.

get_feeds()[source]

Returnera en lista med RSS-flöden att hämta för den här profilen. Varje element i listan måste vara ett 2-elements tupel av formen (titel, url). Om titeln är None eller en tom sträng, används titeln från flödet. Denna metod är användbar om du receptet behöver göra en del bearbetning för att räkna ut lista med inlägg för att hämtar. Om så är fallet, åsidosätt i din underklass.

get_masthead_title()[source]

Åsidosätt i underklass för att använda något annat än recepttiteln

get_masthead_url()[source]

Returnera en URL till redaktionsloggan för denna utgåva eller None. Som standard returneras värdet på medlemmen self.masthead_url som normalt är None. Om du vill att ditt recept ska hämta en redationslogga för e-boken åsidosätter du denna metod i din underklass, eller ställer in medlemsvariabeln self.masthead_url innan denna metod anropas. Redaktionsloggor används i Kindle MOBI-filer.

get_obfuscated_article(url)[source]

Om du ställer in articles_are_obfuscated anropas denna metod med varje artikel-URL. Den ska returnera sökvägen till en fil i filsystemet som innehåller artikeln HTML. Den filen bearbetas av den rekursiva HTML-hämtningsmotorn, så den kan innehålla länkar till sidor/bilder på webben. Alternativt kan du returnera en ordbok av formatet: {’data’: <HTML data>, ’url’: <the resolved URL of the article>}. Detta undviker att behöva skapa tillfälliga filer. url-nyckeln i ordboken är användbar om artikelns effektiva URL skiljer sig från URL-adressen som skickas till den här metoden, till exempel på grund av omdirigeringar. Den kan utelämnas om URL:en är oförändrad.

Denna metod är oftast användbart för webbplatser som försöker göra det svårt att komma åt artikelns innehåll automatiskt.

get_url_specific_delay(url)[source]

Returnera fördröjningen i sekunder innan du hämtar den här webbadressen. Om du programmatiskt vill bestämma fördröjningen för den angivna URL:en, åsidosätt den här metoden i din underklass och returnerar self.delay som standard för URL:er som du inte vill påverka.

Returnerar:

Ett flyttalnummer, fördröjningen i sekunder.

classmethod image_url_processor(baseurl, url)[source]

Utför en del bearbetning av bildwebbadresser (kanske ta bort storleksbegränsningar för dynamiskt genererade bilder osv.) och returnera den föregående URL:en. Returnera None eller en tom sträng för att hoppa över att hämta bilden.

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

Bekväm metod som använder en URL som indexsida och returnerar en BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc] av den.

url_or_raw: Antingen en webbadress eller den hämtade indexsidan som en sträng

is_link_wanted(url, tag)[source]

Returnera True om länken ska följas eller False annars. Som standard höjer NotImplementedError som orsakar hämtningen att ignorera det.

Parametrar:

- url – URL:en som ska följas

- tag – Taggen för vilken URL:en erhölls

parse_feeds()[source]

Skapa en lista med artiklar från listan med flöden som returneras av BasicNewsRecipe.get_feeds(). returnerar en lista med Feed objekt.

parse_index()[source]

Denna metod bör implementeras på recept för att analysera en webbplats istället för flöden skapar en lista med artiklar. Typiska användningsområden är för nyhetskällor som har en ”tryckt upplaga”-webbplats som listar alla artiklar i den aktuella tryckupplagan. Om denna funktion implementeras kommer den att användas istället för BasicNewsRecipe.parse_feeds().

Det måste returnera en lista. Varje element i listan måste vara ett 2-inslag tupel av formen ("flödestitel", lista med artiklar).

Varje lista med artiklar måste innehålla ordböcker av formen:

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

Till exempel, se receptet för hämtning av The Atlantic. Dessutom kan du lägga till ’författare’ till författaren till artikeln.

Om du vill avbryta behandlingen av någon anledning och låta calibre visa användaren ett enkelt budskap istället för ett fel, anropa abort_recipe_processing().

populate_article_metadata(article, soup, first)[source]

Anropas när varje HTML-sida som tillhör artikel hämtas. Avsedd att användas för att få artikelns metadata som författare/sammanfattning/osv. från den analyserad HTML:en (soap).

Parametrar:

- article – Ett objekt av klass calibre.web.feeds.Article. Om du ändrar sammanfattningen, kom ihåg att också ändra text_summary

- soup – Analyserad HTML tillhör den här artikeln

- first – True iff analyserade HTML är den första sidan av artikeln.

postprocess_book(oeb, opts, log)[source]

Kör någon behövd efterbehandling på den analyserade hämtade e-boken.

Parametrar:

- oeb – Ett OEBBook-objekt

- opts – Konverteringsalternativ

postprocess_html(soup, first_fetch)[source]

Denna metod anropas med källan för varje hämtad HTML-fil, efter att den har analyserats för länkar och bilder. Den kan användas för att göra godtyckligt kraftfull efterbearbetning på HTML. Den bör returnera soup efter bearbetatning.

Parametrar:

- soup – En BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] instans innehållande hämtad HTML.

- first_fetch – True om detta är första sidan av en artikel.

preprocess_html(soup)[source]

Denna metod anropas med källan till varje hämtad HTML-fil, innan den analyseras för länkar och bilder. Den anropas efter rensningen som specificerats av remove_tags osv. Det kan användas för att göra godtyckligt kraftfull förbearbetning av HTML. Det ska returnera soup efter bearbetning.

soup: En BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] instans innehållande hämtad HTML.

preprocess_image(img_data, image_url)[source]

Utför viss bearbetning av hämtade bilddata. Denna anropas på rådata innan någon storleksändring görs. Måste returnera bearbetade rådata. Returnera None för att hoppa över bilden.

preprocess_raw_html(raw_html, url)[source]

Denna metod anropas med källa av varje hämtad HTML-fil, innan den analyseras i ett objektträd. raw_html är en unicode sträng som representerar råa HTML-hämtning från webben. url är URL från vilken HTML:en var hämtad.

Observera att den här metoden agerar före preprocess_regexps.

Denna metod måste returnera den bearbetade raw_html som ett unicode-objekt.

classmethod print_version(url)[source]

Ta en url som hänvisar till en webbplats med artikelinnehåll och returnerar URL som hänvisar till en utskrivbar version av artikeln. Som standard gör ingenting. TIll exempel:

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

publication_date()[source]

Använd den här metoden för att ställa in datumet då detta nummer publicerades. Standard till hämtningsögonblicket. Måste returnera ett datetime.datetime-objekt.

skip_ad_pages(soup)[source]

Denna metod anropas med källan till varje hämtad HTML-fil, innan någon av rensningsattribut som remove_tags, keep_only_tags tillämpas. Observera att preprocess_regexps kommer redan ha använts. Det är tänkt att låta receptet att hoppa över annonssidor. Om soup är en annonssida, returnera HTML av den verkliga sidan. Annars returneras None.

soup: En BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] instans innehållande hämtad HTML.

sort_index_by(index, weights)[source]

Bekväm metod för att sortera titlar i index enligt weights. index` sorteras efter plats. Returnerar index.

index: En lista med titlar.

weights: En ordbok som översätter vikt mot titel. Om något titelindex inte har vikt, antas de ha vikten 0.

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

Bekväm metod som använder en BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] Tagga och extrahera texten från den rekursivt, inklusive godtyckliga CDATA-avsnitt och alt-tagg-attribut. Returnera en eventuellt tom Unicode-sträng.

use_alt: Om True försök använd alt attribut för taggar som inte har något textinnehåll
tagg: BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] Tag

articles_are_obfuscated = False

Ställ in till True och implementerar get_obfuscated_article() för att hantera webbplatser som försöker göra det svårt att skrapa fram innehåll.

auto_cleanup = False

Extrahera automatiskt all text från hämtade artikelsidor. Använder algoritmerna från läsbarhetsprojektet. Att ställa in detta till True betyder att du inte behöver oroa dig för att städa upp den hämtade HTML-koden manuellt (även om manuell städning alltid kommer att vara överlägsen).

auto_cleanup_keep = None

Ange element som den automatiska rensningsalgoritmen aldrig ska ta bort. Syntaxen är ett XPath-uttryck. Till exempel:

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

Om True centreras navigationslisten annars är den vänsterjusterad

compress_news_images = False

Ställ in det här till Falsk för att ignorera alla skalnings- och komprimeringsparametrar och passera bilder igenom omodifierade. Om True och andra komprimeringsparametrar behåller sina standardvärden, skalas JPEG-bilder för att passa in skärmdimensionerna som fastställts av utmatningsprofilen och komprimeras till storlek som mest (b * h)/16 där b x h är de skalade bilddimensionerna.

compress_news_images_auto_size = 16

Faktorn som används vid automatisk komprimering av JPEG-bilder. Om den är inställd på Ingen är automatisk komprimering inaktiverad. Annars kommer bilderna att reduceras i storlek till (b * h) / compress_news_images_auto_size byte om möjligt genom att minska kvalitetsnivån, där b x h är bilddimensionerna i pixlar. Minsta JPEG-kvalitet är 5/100 så det är möjligt att denna begränsning inte uppfylls. Denna parameter kan åsidosättas av parametern compress_news_images_max_size som ger en fast maximal storlek för bilder. Observera att om du aktiverar skala_nyheter_images_till_utrustning kommer bilden först att skalas och sedan reduceras dess kvalitet tills dess storlek är mindre än (b*h)/faktor där b och h nu är de skalade bilddimensionerna. Med andra ord, denna komprimering sker efter skalning.

compress_news_images_max_size = None

Ställ in JPEG-kvalitet så att bilderna inte överstiger den angivna storleken (i KByte). Om den är inställd, åsidosättar den här parametern automatisk komprimering via compress_news_images_auto_size. Minsta JPEG-kvalitet kommer att vara 5/100 så det är möjligt att den här begränsningen inte kommer att uppfyllas.

conversion_options = {}

Receptspecifika alternativ för att anpassa konvertering av hämtat innehåll in i en e-bok. Dessa kommer åsidosätta alla användare eller insticksmodul specifika värden, så använd bara om det är absolut nödvändigt. Till exempel:

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

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

Som standard används omslagsbilden som returneras av get_cover_url() som omslaget för tidskriften. Att åsidosätta detta i ditt recept instruerar calibre att göra det hämtade omslaget till en ram vars bredd och höjd uttrycks i procent av det hämtade omslaget. cover_margins = (10, 15, ’#ffffff’) fyller omslaget med en vit marginal 10 pixlar till vänster och höger, 15 pixlar på toppen och botten. Färgnamn definieras på här [https://www.imagemagick.org/script/color.php]. Observera att vit av någon anledning inte alltid fungerar i Windows. Använd istället #ffffff

delay = 0

Standardfördröjningen mellan på varandra följande hämtningar i sekunder. Argumentet kan vara ett flyttal för att indikera en mer exakt tid. Se get_url_specific_delay() för att implementera förseningar per URL.

description = ''

Ett par rader som beskriver innehållet detta recept hämtar. Detta kommer främst att användas i ett användargränssnitt som presenterar en lista med recept.

encoding = None

Ange en åsidosättskodning för webbplatser som har en felaktig teckenuppsättningsspecifikation. Det vanligaste är att ange latin1 och använda cp1252. Om None, försök att identifiera kodningen. Om det är en anropbar, anropas den anropsbara med två argument: Receptobjektet och källan som ska avkodas. Det måste returnera den avkodade källan.

extra_css = None

Ange någon extra CSS som bör läggas till hämtade HTML-filer. Det kommer att införas i <style>-taggar, precis före den avslutande </head>-taggen därigenom åsidosätts alla CSS utom det som deklarerats med hjälp av formatattribut på individuella HTML-taggar. Observera att om du vill att programmässigt skapa extra_css åsidosätt get_extra_css() metoden istället. Till exempel:

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

feeds = None

Förteckning över flöden att hämtar. Kan vara antingen [url1, url2, ...] eller [('title1', url1), ('title2', url2),...]

filter_regexps = []

Lista med reguljära uttryck som avgör vilka länkar att ignorera. Om den är tom ignoreras den. Används endast om is_link_wanted inte implementeras. Till exempel:

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

kommer ta bort alla URL-adresser som har ads.doubleclick.net in sig.

Endast en av BasicNewsRecipe.match_regexps eller BasicNewsRecipe.filter_regexps bör definieras.

handle_gzip = True

Ställ in på False om du inte vill använda gzippade överföringar. Observera att vissa gamla servrar funger dåligt med gzip

ignore_duplicate_articles = None

Ignorera dubbletter av artiklar som finns i mer än ett avsnitt. En dubblettartikel är en artikel som har samma titel och/eller URL. Att ignorera artiklar med samma titel, ställ in detta till:

ignore_duplicate_articles = {'title'}

För att använda URL:er istället, ställ in den till:

ignore_duplicate_articles = {'url'}

För att matcha titel eller URL, ställ in den till:

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

Håll endast specificerade taggar och deras underkategorier. För formatet för att ange en tagg se BasicNewsRecipe.remove_tags. Om denna lista inte är tom, då blir <body> tagg tom och återfylld med taggar som matchar objekt i denna lista. Till exempel:

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

kommer bara ha taggar som har ett id attribut av ”content” eller ”heading”.

language = 'und'

Språket som nyheterna är på. Måste vara en ISO-639-kod, antingen två eller tre tecken lång

masthead_url = None

Som standard använder calibre en standardbild för redaktionsloggan (endast Kindle). Åsidosätt detta i ditt recept för att tillhandahålla en URL som du kan använda som redaktionslogga.

match_regexps = []

Lista med reguljära uttryck som avgör vilka länkar att följa. Om den är tom, ignoreras den. Endast användas om is_link_wanted inte implementeras. Till exempel:

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

kommer matcha alla URL:er som har page=some number i sig.

Endast en av BasicNewsRecipe.match_regexps eller BasicNewsRecipe.filter_regexps bör definieras.

max_articles_per_feed = 100

Högsta antal artiklar att hämta från varje flöde. Den är primärt användbar för flöde som inte har artikeldatum. För de flesta flödes, bör du använda BasicNewsRecipe.oldest_article

needs_subscription = False

Om True kommer gränssnittet att be användaren om ett användarnamn och lösenord för att använda vid hämtning. Om inställningen är ”frivilligt” att använda ett användarnamn och lösenord blir tillval

no_stylesheets = False

Bekväm flagga för att inaktivera laddning av formatmallar för webbplatser som har alltför komplexa formatmallar som är olämpliga för konvertering till e-bokformat. Om True hämtas och bearbetas inte formatmallar

oldest_article = 7.0

Äldsta artikel att hämtar från denna nyhetskälla. I dagar.

preprocess_regexps = []

Lista med regexp substitutionsregler att köras vid hämtning HTML. Varje element i listan bör vara en tupel. Första elementet av tupel bör vara ett kompilerat regujärt uttryck och andra en anropsbar som tar ett matchande objekt och returnerar en sträng för att ersätta träffen. Till exempel:

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

kommer ta bort allt från <!–Article ends here–> till </body>.

publication_type = 'unknown'

Publikationstyp inställd till tidning, tidskrift eller blogg. Om inställt till None, kommer ingen publikationstypsmetadata skrivas till OPF-filen.

recipe_disabled = None

Ändra till en icke-tom sträng för att inaktivera detta recept. Strängen kommer att användas som det inaktiverade meddelandet

recursions = 0

Antal nivåer av länkar att följa på en artikelwebbplats

remove_attributes = []

Lista med attribut att ta bort från alla taggar. Till exempel:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

Om True tas tomma flöden bort från utmatningen. Det här alternativet har ingen effekt om parse_index åsidosätts i underklassen. Den är avsedd endast för recept som returnerar en lista med flöden som använder feeds eller get_feeds(). Den används också om du använder alternativet ignore_duplicate_articles.

remove_javascript = True

Bekväm flagga för att ta bort alla JavaScript-taggar från den hämtad HTML-koden

remove_tags = []

Lista med taggar som ska avlägsnas. Angivna taggar tas bort från hämtad HTML. En tagg anges som en ordbok på formen:

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

Alla tangenter är valbara. För en fullständig förklaring av sökkriterierna, se Beautiful Soup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree] Ett vanligt exempel:

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

Detta tar bort alla <div class=”advert”> taggar och deras underkategorier från hämtade HTML.

remove_tags_after = None

Ta bort alla taggar som förekommer efter angiven tagg. För formatet att ange en tagg se BasicNewsRecipe.remove_tags. Till exempel:

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

kommer ta bort alla taggar efter första elementet med id=”content”.

remove_tags_before = None

Ta bort alla taggar som förekommer före angiven tagg. För formatet att ange en tagg se BasicNewsRecipe.remove_tags. Till exempel:

remove_tags_before = dict(id='content')

kommer ta bort alla taggar före första elementet med id=”content”.

requires_version = (0, 6, 0)

Lägsta version av calibre som behövs för detta recept

resolve_internal_links = False

Om den är inställd till True ändras länkar i hämtade artiklar som hänvisar till andra hämtade artiklar ändras till att hänvisa till den hämtade kopian av artikeln istället än dess ursprungliga URL. Om du ställer in detta till True, kan du också behöva implementera canonicalize_internal_url() för att fungera med webbadresschemat för din specifika webbplats.

reverse_article_order = False

Vänd om ordningen på artiklar i varje flöde

scale_news_images = None

Största dimensioner (w, h) att skala bilder till. Om scale_news_images_to_device är True så är detta inställt på enhetens skärmmått för utskriftsprofil om det inte finns någon profil som, i vilket fall det är kvar på oavsett värde den har tilldelats (standard None).

scale_news_images_to_device = True

Skala om bilder för att passa enhetens skärmmått inställda av utmatningsprofilen. Ignoreras om ingen utmatningsprofil är angiven.

simultaneous_downloads = 5

Antal samtidiga hämtningar. Ställ in till 1 om servern är kräsen. Sänks automatiskt till 1 om attr:BasicNewsRecipe.delay > 0

summary_length = 500

Högsta antal tecken i den korta beskrivningen

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    '

CSS som används för att utforma mallar, dvs. navigeringsfältet och innehållsförteckningen. Istället för att åsidosätta denna variabel bör du använda extra_css i ditt recept för att anpassa utseende och känsla.

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

Formatsträngen för datumet som visas på första sidan. Som standard: Day_Name, Day_Number Month_Name Year

timeout = 120.0

Tidsbegränsning för att hämta filer från servern i sekunder

title = 'Okänd nyhetskälla'

Titel att använda för e-boken

use_embedded_content = None

Normalt försöker vi uppskatta om ett flöde har hela artiklar inbäddade i den baserat på längden av inbäddat innehåll. Om None, sedan standard används gissning. Om True då antar vi alltid flöden har inbäddat innehåll och om` False` kan vi alltid utgå från flödet inte har inbäddat innehåll.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- E-bokvisaren

E-bokvisaren
calibre inkluderar en inbyggd e-bokvisare som kan visa de större e-bokformaten. E-bokvisaren är väldigt anpassningsbar och har många avancerade funktioner.

- Starta e-bokvisaren

- Navigera omkring i en e-bok

- Markera text

- Högläsning

- Söka i texten

- Följa länkar med endast tangentbordet

- Anpassa utseendet och känslan av din läsupplevelse

- Ordboksuppslagning

- Kopiera text och bilder

- Zooma in på bilder

- Tangentbords- genvägar

- Inte återflödbart innehåll

- Designa din bok för att fungera bra med calibre-visaren

Starta e-bokvisaren

Du kan visa någon av böckerna i ditt calibre-bibliotek genom att välja boken och trycka på knappen Visa. Detta öppnar boken i e-bokvisaren. Du kan också starta e-bokvisaren själv från startmenyn i Windows. På macOS kan du fästa den till dockan och starta den därifrån. På Linux kan du använda startprogrammet i skrivbordsmenyerna eller köra kommandot ebook-viewer.

Navigera omkring i en e-bok

Du kan ”vända sidor” i en bok genom att antingen:

- Klicka i den vänstra eller högra marginalen eller sidan med muspekaren

- Trycka på blanksteg, Page Up, Page Down eller piltangenterna

- På en pekskärm genom att trycka på texten eller dra åt vänster och höger

Du kan komma åt visarens kontroller genom att antingen:

- Högerklicka på texten

- Genom att trycka på Esc eller tangenterna Meny

- På en pekskärm genom att trycka på den övre tredjedelen av skärmen

Visaren har två lägen, ”siddelat ” och ”flöde”. I siddelat läge presenteras bokinnehållet som sidor, som liknar en pappersbok. I flödesläge texten presenteras kontinuerligt, som i en webbläsare. Du kan växla mellan dem med hjälp av visarens Inställningar under Sidutformning eller genom att trycka på Ctrl+M.

Bokmärken

När du är mitt i en bok och stänger e-bokvisaren kommer den att komma ihåg var du slutade läsa och återvända dit nästa gång du öppnar boken. Du kan också ställa in bokmärken i boken genom att använda knappen Bokmärken i e-bokvisarens kontroller eller trycka på Ctrl+B. När du läser böcker med EPUB-format sparas dessa bokmärken faktiskt i själva EPUB-filen. Du kan lägga till bokmärken och sedan skicka filen till en vän. När de öppnar filen kommer de att kunna se dina bokmärken. Du kan stänga av det här beteendet i avsnittet Diverse i inställningarna för visaren.

Innehållsförteckning

Om boken du läser har en innehållsförteckning kan du komma åt den genom att trycka på knappen Innehållsförteckning. Detta kommer att ta fram en lista med avsnitt i boken. Du kan klicka på någon av dem för att hoppa till den delen av boken.

Navigering efter plats

E-böcker, till skillnad från pappersböcker, har inget begrepp om sidor. Du kan referera till exakta platser i e-böcker med hjälp av funktionaliteten Gå till → Plats i visarens kontroller.

Du kan använda denna platsinformation för att entydigt hänvisa till delar av böckerna när du diskuterar det med vänner eller hänvisar till det i andra verk. Du kan ange dessa platser under Gå till → Plats i visarens kontroller.

Det finns en URL som du kan kopiera till urklipp och klistra in i andra program eller dokument. Genom att klicka på den här webbadressen öppnas boken i calibre E-book visaren på den aktuella platsen.

Om du klickar på länkar inne i e-boken kommer du kunna ta dig till olika delar av boken, som slutnotering, kan du använda dig av Tillbaka- och Framåt-knapparna i övre vänstra hörnet av visarens kontroller. Dessa knappar beter sig precis som dem i en webbläsare.

Referensläge
calibre har också ett praktiskt Referensläge. Du kan slå på det genom att klicka på knappen Referensläge i visarens kontroller. När du har gjort detta kommer varje stycke att ha ett unikt nummer som visas i början, som består av avsnitt- och styckenumren.

Du kan använda detta nummer för att entydigt hänvisa till delar av böckerna när du diskuterar det med vänner eller hänvisar till det i andra verk. Du kan ange dessa nummer i Gå till funktion för att navigera till en särskild referensplats.

Markera text

När du väljer text i visaren visas ett liten popup-fält bredvid markeringen. Du kan klicka på markeringsknappen i den fältet för att skapa en markering. Du kan lägga till anteckningar och ändra färgen på markeringen. På en pekskärm, tryck länge på ett ord för att välja det och visa popup-fältet. När du är i markeringsläget kan du ändra vilken text som markeras med hjälp av pekskärmsvänliga markeringshandtag. Dra handtagen till den övre eller nedre marginalen för att bläddra medan du markerar. Du kan också Skift+klicka och högerklicka för att utöka markeringen, särskilt användbart för flersidiga markeringar.

Du kan använda knappen Markeringar i visarens kontroller för att visa en separat panel med en lista över alla markeringar i boken, sorterade efter kapitel.

Du kan bläddra bland alla markeringar i hela ditt calibre-bibliotek genom att högerklicka på knappen Visa och välja Bläddra bland noteringar.

Slutligen, om du använder calibre-innehållsservern i webbläsarvisaren, kan du låta visaren synkronisera sina noteringar med webbläsarvisaren genom att gå till Inställningar → Diverse i visarens inställningarna och ange användarnamnet för innehållsservervisaren att synkronisera med. Använd specialvärdet * för att synkronisera med anonyma användare.

Högläsning

Visaren kan läsa boktexten högt. För att använda den kan du helt enkelt klicka på Högläsning i visningskontrollerna för att börja läsa boktexten högt. Ordet som för närvarande läses är markerat. Tal syntetiseras från texten med hjälp av dina operativsystemstjänster för text-till-tal. Du kan ändra rösten som används genom att klicka på kugghjulsikonen i fältet som visas medan Högläsning är aktivt.

Du kan också läsa upp markerade avsnitt genom att lägga till knappen Högläsning i markeringsfältet i visarens inställningarna under Markeringsbeteende.

Observera

Stöd för text-till-tal i webbläsare är väldigt ofullständig och felaktig, så hur bra Högläsning kommer att fungera i webbläsarvisaren är beroende av hur väl den underliggande webbläsaren stöder text-till-tal. I synnerhet fungerar inte markering av det aktuella ordet och om du ändrar hastighet eller röst kommer läsningen att börja om från början.

Observera

På Linux kräver Högläsning att Speech Dispatcher [https://freebsoft.org/speechd] är installerat och fungerar.

Observera

I Windows kanske inte alla installerade röster är synliga för SAPI-delsystemet som används för text-till-tal. Det finns instruktioner för att göra alla röster synliga [https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108].

Söka i texten

Visaren har mycket kraftfulla sökmöjligheter. Tryck på tangenterna Ctrl+F eller gå till visarens kontroller och klicka på sök. Den enklaste formen av sökning är att bara söka efter vilken text du än skriver i textrutan. De olika sökformerna väljs av söklägesrutan under sökinmatningen. Tillgängliga lägen är:

1. Innehåller - Det enklaste standardläget. Texten som skrivs in i sökrutan söks efter var som helst. Alla skiljetecken, accenter och mellanslag ignoreras. Till exempel kommer sökningen: Pena att matcha alla följande: penal, pen a, pen.a och Peña. Om du markerar rutan Skiftlägeskänslig ignoreras inte längre accenter, mellanslag och skiljetecken.

2. Hela ord - Söker efter hela ord. Så till exempel kommer sökningen pena att matcha ordet Peña men inte ordet Penal. Som med Innehåller sökningar ovan ignoreras accenter och skiljetecken såvida inte Skiftlägeskänslig är markerad.

3. Närliggande ord - Söker efter hela ord som är nära varandra. Så till exempel kommer sökningen calibre cool att matcha platser där orden calibre och cool förekommer inom sextio tecken från varandra. För att ändra antalet tecken lägg till det nya numret i slutet av listan med ord. Till exempel kommer calibre cool awesome 120 att matcha platser där de tre orden förekommer inom 120 tecken från varandra. Observera att skiljetecken och accenter inte ignoreras för dessa sökningar.

4. Regex - Tolkar söktexten som ett reguljärt uttryck. För att lära dig mer om hur du använder reguljära uttryck, se manualen.

Följa länkar med endast tangentbordet

E-bokvisaren har ett Tipsläge som låter dig klicka på länkar i texten utan att använda muspekaren. Tryck på Alt+F och alla länkar i den aktuella skärmen markeras med ett nummer eller en bokstav över dem. Tryck på bokstaven på ditt tangentbord för att klicka på länken. Genom att trycka på Esc avbryts Tipsläge utan att välja någon länk.

Om mer än trettiofem länkar visas på skärmen kommer vissa av dem att ha flera bokstäver, i vilket fall skriver du den första och den andra eller den första och trycker på Enter för att aktivera. Du kan också använda Baksteg för att ångra ett fel vid skrivning.

Anpassa utseendet och känslan av din läsupplevelse

Du kan ändra teckensnittsstorlekar i farten med hjälp av Teckensnittsstorlek i visarens kontroller eller Ctrl++ eller Ctrl+- eller hålla Ctrl- tangenten och använda mushjulet.

Färger kan ändras i avsnittet Färger i inställningarna för visaren.

Du kan ändra antalet sidor som visas på skärmen samt sidmarginaler i Sidutformning i visarens inställningar.

Du kan visa anpassade sidhuvud och sidfot som tid kvar att läsa, aktuell kapiteltitel, bokposition osv. via avsnittet Sidhuvuden och sidfötter i inställningarna för visaren.

Mer avancerad anpassning kan uppnås genom inställningarna Format. Här kan du ange en bakgrundsbild som ska visas under texten och även en formatmall som du kan ställa in som kommer att tillämpas på varje bok. Med hjälp av det kan du göra saker som ändra styckeformat, textjustering osv. Exempel på anpassade formatmallar som används av calibre-användare, se forumet [https://www.mobileread.com/forums/showthread.php?t=51500].

Ordboksuppslagning

Du kan slå upp betydelsen av ord i den aktuella boken genom att dubbelklicka eller trycka länge på ordet du vill slå upp och sedan klicka på uppslagsknappen som ser ut som ett bibliotek.

Kopiera text och bilder

Du kan välja text och bilder genom att dra innehållet med din mus och sedan högerklicka och välja Kopiera för att kopiera till urklipp. Det kopierade materialet kan klistras in i ett annat program som oformaterad text och bilder.

Zooma in på bilder

Du kan zooma in för att visa en bild i full storlek i ett separat fönster genom att antingen dubbelklicka eller trycka länge på den. Du kan också högerklicka på den och välja Visa bild.

Tangentbords- genvägar

The viewer has extensive keyboard shortcuts, like the rest of calibre. They can be customised in the viewer Preferences. The default shortcuts are listed below:

Keyboard shortcuts for the calibre viewer

Tangentbordsgenväg

Åtgärd

Home, Ctrl+ArrowUp, Ctrl+ArrowLeft

Scroll to the start of the current file in a multi file book

Ctrl+Home

Bläddra till början av boken

Ctrl+End

Bläddra till slutet av boken

End, Ctrl+ArrowDown, Ctrl+ArrowRight

Scroll to the end of the current file in a multi file book

ArrowUp

Rulla bakåt, jämnt i flödesläge och med skärmuppfyllningar i siddelat läge

ArrowDown

Rulla framåt, jämnt i flödesläge och med skärmuppfyllningar i siddelat läge

ArrowLeft

Scroll leftwards by a little in flow mode and by a page in paged mode

ArrowRight

Scroll rightwards by a little in flow mode and by a page in paged mode

PageUp, Shift+Spacebar

Bläddra bakåt med skärmuppfyllningar

PageDown, Spacebar

Bläddra framåt med skärmuppfyllningar

Ctrl+PageUp

Bläddra till föregående avsnitt

Ctrl+PageDown

Bläddra till nästa avsnitt

Alt+ArrowLeft

Tillbaka

Alt+ArrowRight

Framåt

Ctrl+T

Toggle Table of Contents

Ctrl+S

Högläsning

Alt+P

Change settings quickly by creating and switching to profiles

Alt+f

Följ länkar med tangentbordet

Ctrl+C

Kopiera till urklipp

Alt+C

Kopiera aktuell plats till urklipp

Ctrl+Shift+C

Kopiera aktuell plats som calibre://-URL till urklipp

/, Ctrl+f, Cmd+f

Påbörja sökning

F3, Enter

Hitta nästa

Shift+F3, Shift+Enter

Hitta föregående

Ctrl+Plus, Meta+Plus

Öka teckensnittsstorleken

Ctrl+Minus, Meta+Minus

Minska teckensnittsstorleken

Ctrl+0

Återställ standardteckensnittsstorlek

Ctrl+]

Öka antalet sidor per skärm

Ctrl+[

Minska antalet sidor per skärm

Ctrl+Alt+C

Ställ in antalet sidor per skärm automatiskt

F11, Ctrl+Shift+F

Växla helskärmsläge

Ctrl+M

Växla mellan siddelat läge och flödesläge för textutformning

Ctrl+W

Visa/dölj rullisten

Ctrl+X

Växla referensläge

Ctrl+B

Visa/dölj bokmärken

Ctrl+Alt+B

Nytt bokmärke

Ctrl+N, Ctrl+E

Visa bokens metadata

Ctrl+Alt+F5, Ctrl+Alt+R

Ladda om boken

Ctrl+Shift+ArrowRight

Ändra den aktuella markeringen framåt med ett ord

Ctrl+Shift+ArrowLeft

Ändra den aktuella markeringen bakåt med ett ord

Shift+ArrowRight

Ändra den aktuella markeringen framåt med ett tecken

Shift+ArrowLeft

Ändra den aktuella markeringen bakåt med ett tecken

Shift+ArrowDown

Ändra den aktuella markeringen framåt med en rad

Shift+Home

Utvidga den aktuella markeringen till början av raden

Shift+End

Utvidga den aktuella markeringen till slutet av raden

Ctrl+A

Markera allt

Shift+ArrowUp

Ändra den aktuella markeringen bakåt med en rad

Ctrl+Shift+ArrowDown

Ändra den aktuella markeringen framåt med ett stycke

Ctrl+Shift+ArrowUp

Ändra den aktuella markeringen bakåt med ett stycke

Esc, MenuKey

Visa e-bokvisarens kontroller

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

Visa e-bokvisarens inställningar

Ctrl+G, ;, :

Gå till en specifik bokplats eller position

Ctrl+Spacebar

Toggle auto-scroll

Alt+ArrowUp

Auto scroll faster

Alt+ArrowDown

Auto scroll slower

Ctrl+I

Visa/dölj inspektör

Ctrl+L

Visa/dölj orduppslagningspanelen

Ctrl+Q (Cmd+Q on macOS)

Avsluta

Ctrl+P

Skriv bok till PDF

Ctrl+F11

Visa/dölj verktygsfältet

Ctrl+H

Växla markeringspanelen

Ctrl+D

Redigera denna bok

Inte återflödbart innehåll

Vissa böcker har mycket brett innehåll som inte kan delas upp vid sidgränser. Till exempel tabeller eller <pre>-taggar. I sådana fall bör du växla visaren till flödesläge genom att trycka på Ctrl+M för att läsa detta innehåll. Alternativt kan du också lägga till följande CSS till sektionen Format i visarens inställningar för att tvinga visaren att bryta upp textrader i <pre>-taggar:

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

Designa din bok för att fungera bra med calibre-visaren
calibre-visaren ställer in klassen is-caliber-viewer i rotelementet. Så du kan skriva CSS-regler som endast gäller för den. Dessutom kommer visaren att ställa in följande klasser i elementet body:

body.calibre-viewer-dark-colors

Ställ in vid användning av ett mörkt färgschema

body.calibre-viewer-light-colors

Ställ in vid användning av ett ljust färgschema

body.calibre-viewer-paginated

Ställ in i siddelat läge

body.calibre-viewer-scrolling

Ställ in i flödesläge (icke-paginerat)

body.calibre-footnote-container

Set when displaying a popup footnote

Slutligen kan du använda färgerna i calibres färgschema via CSS-variabler [https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties]. calibre-visaren definierar följande variabler: --calibre-viewer-background-color, --calibre-viewer-foreground-color och valfritt --calibre-viewer-link-color i färgteman som definierar en länkfärg.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Konvertering av e-bok

Konvertering av e-bok
calibre har ett konverteringssystem som är utmatningsformat för att vara mycket lätt att använda. Normalt lägger du bara till en bok till calibre, klicka på konvertera och calibre kommer att försöka skapa utmatning som är så nära inmatningen som möjligt. calibre accepterar dock ett mycket stort antal inmatningsformat, som inte alla som är lika lämpliga som andra för konvertering till e-böcker. När det gäller sådana inmatningsformat, eller om du bara vill ha större kontroll över konverteringssystemet, har calibre en hel del alternativ för att finjustera konverteringen. Observera dock att calibres konverteringssystem inte är en ersättning för en komplett e-bokredigerare. För att redigera e-böcker rekommenderar jag att du först konverterar dem till EPUB eller AZW3 med calibre och sedan använder funktionen Redigera bok för att få dem i perfekt form. Du kan sedan använda den redigerade e-boken som inmatning för konvertering till andra format i calibre.

Det här dokumentet kommer huvudsakligen att hänvisa till konverteringsinställningarna som finns i konverteringsdialogrutan, på bilden nedan. Alla dessa inställningar är även tillgängliga via kommandoradsgränssnittet för konvertering, dokumenterat i ebook-convert. I calibre kan du få hjälp med vilken enskild inställning som helst genom att hålla muspekaren över den, ett verktygstips kommer att visas som beskriver inställningen.

Innehåll

- Introduktion

- Utseende & känsla

- Sidinställning

- Heuristisk bearbetning

- Söka & ersätta

- Struktrurdetektering

- Innehållsförteckning

- Använda bilder som kapitelrubriker vid konvertering av HTML-inmatningsdokument

- Använda taggattribut för att leverera texten för posterna i innehållsförteckningen

- Hur alternativ ställs in/sparas för konvertering

- Formatspecifika tips

Introduktion

Det första att förstå om konverteringssystemet är att det är utformat som en process. Schematiskt ser det ut så här:

Inmatningsformatet konverteras först till XHTML med lämplig inmatningsinsticksmodul. Denna HTML omvandlas I det sista steget konverteras den bearbetade XHTML-en till det angivna utmatningsformatet med lämplig utmatningsinsticksmodul. Resultaten av konverteringen kan variera kraftigt, beroende på inmatningsformatet. Vissa format konverteras mycket bättre än andra. En lista med de bästa källformaten för konvertering finns här.

Omvandlingarna som verkar på XHTML-utmatningen är där allt arbete sker. Det finns olika omvandlingar, till exempel för att infoga bokens metadata som en sida i början av boken, för att upptäcka kapitelrubriker och automatiskt skapa en innehållsförteckning, för att proportionellt justera teckensnittsstorlekar, och så vidare. Det är viktigt att minnas att alla omvandlingar verkar på XHTML-utmatning av inmatningsinsticksmodulen, inte på själva inmatningsfilen. Så, till exempel, om du ber calibre att konvertera en RTF-fil till EPUB, kommer den först att konverteras till XHTML internt, de olika omvandlingarna kommer att tillämpas på XHTML och sedan kommer itmatningsinsticksmodulen att skapa EPUB-filen, vilket automatiskt skapar alla metadata, innehållsförteckning, och så vidare.

Du kan se den här processen i aktion genom att använda felsökningsalternativet . Ange bara sökvägen till en mapp för felsökningsutmatning. Under konverteringen kommer calibre att placera XHTML som skapas av de olika stegen i konverteringsprocessen i olika undermappar. De fyra undermapparna är:

Stadier av konverteringsprocessen

Mapp

Beskrivning

inmatning

Den innehåller HTML-utmatning från inmatningsinsticksmodulen. Använd detta för att felsöka inmatningsinsticksmodulen.

analyserad

Resultatet av förbehandling och konvertering till XHTML-utmatning från inmatningsinsticksmodulen. Används för att felsöka strukturdetektering.

struktur

Efterstrukturell detektering, men innan CSS-utplattning och teckensnittsstorlekskonvertering. Används för att felsöka teckensnittsstorlekskonvertering och CSS-omvandlingar.

bearbetad

Precis innan e-boken passerar till Utmatningsinsticksmodulen. Används för att felsöka Utmatningsinsticksmodulen.

Om du vill redigera inmatningsdokumentet lite innan calibre konverterar det, är det bästa att göra redigera filerna i inmatning undermapp och sedan packa upp och använda ZIP-filen som inmatningsformat för efterföljande konverteringar. För att göra detta använder Edit meta information dialogrutan för att lägga ZIP-filen som ett format för boken och sedan, i det övre vänstra hörnet i konverteringsdialogrutan väljer ZIP som inmatningsformat.

Det här dokumentet kommer huvudsakligen att behandla de olika omvandlingarna som fungerar på mellanliggande XHTML och hur man anpassar dem. I slutet finns några tips som är specifika för varje in-/utmatningsformat.

Utseende & känsla

Innehåll

- Teckensnitt

- Text

- Layout

- Formgivning

- Omvandla format

- Omvandla HTML

Denna grupp av alternativ anpassar olika aspekter av utseendet och känslan av den konverterade e-boken.

Teckensnitt

En av de trevligaste inslagen i e-läsupplevelse är möjligheten att enkelt justera teckensnittsstorlek för att passa individuella behov och ljusförhållanden. calibre har sofistikerade algoritmer för att säkerställa att alla böcker det avger har konsistenta teckensnittsstorlekar, oavsett vilken teckensnittsstorlekar anges i inmatningsdokumentet.

Grundteckensnittsstorleken för ett dokument är den vanligaste teckensnittsstorleken i det dokumentet, dvs. storleken på den största delen av texten i dokumentet. När du anger en Grundteckensnittsstorlek, skalar calibre automatiskt om alla teckensnittsstorlekar i dokumentet proportionellt, så att den vanligaste teckensnittsstorleken blir den angivna grundteckensnittsstorleken och andra teckensnittsstorlekar skalas på lämpligt sätt. Genom att välja en större grundteckensnittsstorlek kan du göra teckensnitten i dokumentet större och vice versa. När du ställer in grundteckensnittsstorleken, för bästa resultat, bör du också ställa in teckensnittsstorleksnyckeln.

Normalt väljer calibre automatiskt en grundteckensnittsstorlek som är lämplig för utmatningsprofilen du har valt (se Sidinställning). Du kan dock åsidosätta detta här om standardvärdet inte är lämplig för dig.

Med alternativet Teckensnittsstorleksnyckel kan du anpassa hur icke-grundteckensnittsstorlekar skalas om. Teckensnittets omskalningsalgoritm fungerar med hjälp av en teckensnittsstorleksnyckel, vilket helt enkelt är en kommaseparerad lista över teckensnittsstorlekar. Teckensnittsstorleksnyckel anger för calibre hur många ”steg” större eller mindre en given teckensnittsstorlek bör jämföras med grundteckensnittsstorleken. Tanken är att det ska finnas ett begränsat antal teckensnittsstorlekar i ett dokument. Till exempel en storlek för huvudtext, ett par storlekar för olika nivåer av rubriker och ett par storlekar för upphöjd/nersänkt teckensnitt och fotnoter. Teckensnittsstorleksnyckeln tillåter calibre att dela upp de teckensnittsstorlekar i inmatningsdokumenten i separata ”fack” som motsvarar de olika logiska teckensnittsstorlekarna.

Låt oss illustrera med ett exempel. Antag att källdokumentet vi konverterar producerades av någon med utmärkt syn och har en grundteckensnittsstorlek på 8 punkter. Det innebär att huvuddelen av texten i dokumentet är dimensionerad för 8 punkter medan rubriker är något större (säg 10 och 12 punkter) och fotnoter något mindre på 6 punkter. Nu om vi använder följande inställningar:

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

Utmatningsdokumentet kommer att ha en grundteckensnittsstorlek på 12 punkter, rubrikerna 14 och 16 punkter och fotnoter 8 punkter. Anta nu att vi vill göra att den största rubrikstorleken sticker ut mer och göra fotnoter lite större också. För att uppnå detta bör teckensnittsnyckeln ändras till:

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

De största rubrikerna blir nu 18 punkter, medan sidnoter blir 9 punkter. Du kan spela med dessa inställningar för att försöka lista ut vad som skulle vara optimalt för dig med hjälp av tecken omskalningsguiden som kan nås genom att klicka på den lilla knappen bredvid Teckensnittsstorleksnyckel.

Alla omskalningar av teckensnittsstorleken i konverteringen kan även stängas av här, om du vill bevara teckensnittsstorlekar i inmatningsdokumentet.

En relaterad inställning är Radavstånd. Radavstånd styr den vertikala höjden på raderna. Som standard utför (ett radavstånd av 0) ingen manipulation av radavstånd. Om du anger ett icke-standardvärde, kommer radavstånd ställas in på alla platser som inte anger sina egna radavstånd. Det här är dock något av ett trubbigt vapen och bör användas sparsamt. Om du vill justera radavstånd för vissa avsnitt av inmatning, är det bättre att använda Extra CSS.

I det här avsnittet kan du också berätta för calibre att bädda in några refererade teckensnitt i boken. Detta gör det möjligt för teckensnitten att fungera på läsarenheter, även om de inte är tillgängliga på enheten.

Text

Text kan antingen justeras eller inte. Justerad text har extra blanksteg mellan ord för att ge en jämn högermarginal. Vissa människor föredrar justerad text, andra gör det inte. Normalt kommer calibre bevara justeringen i originaldokuemtet. Om du vill åsidosätta det kan du använda alternativet Textjustering i det här avsnittet.

Du kan också be calibre att Förbättra skiljeteckenhantering vilket kommer att ersätta vanliga citattecken, bindestreck och ellipser med deras typografiskt korrekta alternativ. Observera att den här algoritmen inte är perfekt så det är värt att granska resultaten. Det omvända, Avförbättra skiljeteckenhantering, är också tillänglig.

Slutligen finns det Inmatningsteckenkodning. Äldre dokument anger ibland inte deras teckenkodning. Vid konvertering kan detta leda till att icke-engelska tecken eller specialtecken som typografiska citattecken skadas. calibre försöker att automatiskt upptäcka källdokumentets teckenkodning, men det lyckas inte alltid. Du kan tvinga den att anta en viss teckenkodning med den här inställningen. cp1252 är en vanlig kodning för dokument som produceras med Windows-programvara. Du bör också läsa Hur konverterar jag min fil som innehåller icke-engelska tecken eller typografiska citattecken? för mer om kodningsfrågor.

Layout

Normalt återges stycken i XHTML med en blnakrad mellan dem och inget inledande textindrag. calibre har ett par alternativ för att anpassa detta. Ta bort avstånd mellan stycken ser till att alla stycken inte har något avstånd mellan stycken. Den ställer också in textindraget till 1.5em (kan ändras) för att markera början på varje stycke. Infoga tom rad gör det motsatta och garanterar att det finns exakt en tom rad mellan varje par av stycken. Båda dessa alternativ är mycket omfattande, tar bort avstånd eller infogar det för alla stycken (tekniska <p>- och <div>-taggar). Detta är så att du bara kan ställa in alternativet och vara säker på att den fungerar som utlovat, oavsett hur rörig inmatningsfilen är. Det enda undantaget är när inmatningsfilen använder hårda radbrytningar för att implementera avstånd mellan stycken.

Om du vill ta bort avstånd mellan alla stycken, förutom några få utvalda, använd inte dessa alternativ. Lägg istället till följande CSS-kod i Extra CSS:

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

Markera sedan styckena i ditt källdokument som behöver avstånd med class=”spacious”. Om ditt inmatningsdokument inte är i HTML, använd alternativet Felsökning, som beskrivs i inledningen för att hämta HTML (använd undermappen input).

Ett annat användbart alternativ är Konvertera data i tabeller till linjära data. Vissa dåligt utformade dokument använder tabeller för att anpassa utformningen för texten på sidan. Vid konvertering har dessa dokument ofta text som går utanför sidan och andra artefakter. Detta alternativ extraherar innehållet från tabellerna och presenterar det linjärt. Observera att det här alternativet linjäriserar alla tabeller, så använd det bara om du är säker på att inmatningsdokumentet inte använder tabeller för legitima ändamål, som att presentera tabellinformation.

Formgivning

Alternativet Extra CSS låter dig ange godtycklig CSS som kommer att tillämpas på alla HTML-filer i inmatningen. Denna CSS appliceras med mycket hög prioritet och bör åsidosätta de flesta CSS närvarande i själva inmatningsdokumentet. Du kan använda denna inställning för att finjustera presentation/utformningen av ditt dokument. Till exempel om du vill att alla stycken i klassen slutnotering att vara högerställda, lägg bara till:

.endnote { text-align: right }

eller om du vill ändra indraget för alla stycken:

p { text-indent: 5mm; }

Extra CSS är ett mycket kraftfullt alternativ, men du behöver en förståelse för hur CSS fungerar för att använda den till sin fulla potential. Du kan använda alternativet felsök processalternativet som beskrivs ovan för att se vad CSS finns i ditt inmatningsdokument.

Ett enklare alternativ är att använda Filtrera formatinformation. Det här låter dig ta bort alla CSS-egenskaper för de angivna typerna från dokumentet. Du kan till exempel använda den för att ta bort alla färger eller teckensnitt.

Omvandla format

Detta är den mest kraftfulla formatrelaterade bekvämligheten. Du kan använda den för att definiera regler som ändrar format baserat på olika villkor. Till exempel kan du använda den för att ändra alla gröna färger till blåa, eller ta bort alla feta stilar från text eller färga alla rubriker en viss färg, osv.

Omvandla HTML

Liknar att omvandla format, men låter dig göra ändringar i HTML-innehållet i boken. Du kan ersätta en tagg med en annan, lägga till klasser eller andra attribut till taggar baserat på deras innehåll, osv.

Sidinställning

Alternativen för sidinställningar är till för att anpassa skärmutformning, som marginaler och skärmstorlekar. Det finns alternativ till sidmarginalsinställningar, som kommer att användas av utmatning insticksmodulen, om det valda utmatningsformatet stödjer sidmarginaler. Dessutom bör du välja en inmatningsprofil och en utskriftsprofil. Båda uppsättningarna av profiler i princip behandlar hur man ska tolka mått i dokumentets inmatning/utmatning, skärmstorlekar och standardteckensnitts omskalningsnycklar.

Om du vet att filen du konverterar var avsedd att användas på en viss enhet/mjukvaruplattform väljer du motsvarande inmatningsprofil, annars väljer du bara standardinmatningsprofilen. Om du vet att filerna du producerar är avsedda för en viss enhetstyp väljer du motsvarande utmatningsprofil. Annars väljer du en av de generiska utmatningsprofilerna. Om du konverterar till MOBI eller AZW3 vill du nästan alltid välja en av Kindle-utmatningsprofilerna. Annars är det bästa alternativet för moderna e-bokläsenheterna att välja utmatningsprofilen Generisk e-bläck HD.

Utmatningsprofilen styr även skärmstorleken. Detta kommer att orsaka, till exempel bilder som automatiskt storleksändras vara tjänliga till skärmen i vissa utmatningsformat. Så välj en profil av en enhet som har en skärmstorlek som liknar din enhet.

Heuristisk bearbetning

Heuristisk bearbetning erbjuder en mängd olika funktioner som kan användas för att försöka upptäcka och åtgärda vanliga problem i dåligt formaterade inmatningsdokument. Använd dessa funktioner om ditt inmatningsdokument lider av dålig formatering. Eftersom dessa funktioner är beroende av gemensamma mönster, var medveten om att ett alternativ i vissa fall kan leda till sämre resultat, så använd med försiktighet. Som ett exempel, kommer flera av dessa alternativ ta bort alla icke-blankstegsbrytande enheter, eller kan innefatta falska positiva resultat avseende funktionen.

Aktivera heuristisk bearbetning

Det här alternativet aktiverar calibres :guilabel:`heuristiska bearbetnings`steg för konverteringsprocessen. Det här måste vara aktiverat för att olika underfunktioner ska kunna tillämpas

Radbrytningar

Aktivering av det här alternativet gör att calibre försöker upptäcka och korrigera hårda radbrytningar som finns i ett dokument med ledtrådar som skiljetecken och radlängd. calibre försöker först att upptäcka om hårda radbrytningar finns, om de inte verkar finnas kommer calibre inte att försöka att bryta rader. Radbrytningsfaktorn kan minskas om man vill ”tvinga” calibre att bryta rader.

Faktor för radbrytningar

Det här alternativet styr algoritmen calibre använder för att ta bort hårda radbrytningar. Till exempel, om värdet av det här alternativet är 0,4, innebär det att calibre tar bort hårda radbrytningar från slutet av rader vars längd är mindre än längden på 40% av alla rader i dokumentet. Om ditt dokument bara har några radbrytningar som behöver korrigering, bör detta värde bör minskas till någonstans mellan 0,1 och 0,2.

Identifiera och markera oformaterade kapitelrubriker och underrubriker

Om ditt dokument inte har kapitelrubriker och titlar formaterade annorlunda än resten av texten kan calibre använda det här alternativet för att försöka upptäcka dem och omge dem med rubriktaggar. <h2>taggar används för kapitelrubriker; <h3>taggar används för några titlar som upptäcks.

Den här funktionen skapar inte en innehållsförteckning, men i många fall kommer den att orsaka calibres standardinställningar för kapiteldetektering att korrekt detektera kapitel och bygga en innehållsförteckning. Justera XPath under Strukturdetektering om en innehållsförteckning inte skapas automatiskt. Om det inte finns några andra rubriker i dokumentet skulle det vara det enklaste sättet att skapa en innehållsförteckning för dokumentet att ställa in ”//h:h2” under strukturdetektering.

De insatta rubriker är inte formaterad, för att tillämpa formatering använd Extra CSS alternativet under konverteringsinställningar under Utseende och känsla. Till exempel för att centrera rubriktaggar, använd följande:

h2, h3 { text-align: center }

Numrera om sekvenser av <h1> eller <h2> taggar

Vissa utgivare formaterar kapitelrubriker som använder flera <h1> eller <h2> taggar sekventiellt. calibres standardkonverteringsinställningar kommer orsaka att sådana titlar delas upp i två delar. Det här alternativet kommer att numrera rubriktaggarna för att undvika delning.

Ta bort tomma rader mellan stycken

Detta alternativ gör att calibre analyserar tomma rader som ingår i dokumentet. Om varje stycke är sammanflätat med en tom rad, tar calibre bort alla de tomma styckena. Sekvenser av flera tomma rader betraktas som scenbrytningar och behålls som ett enda stycke. Det här alternativet skiljer sig från alternativet Ta bort avstånd mellan stycken under Utseende och känsla genom att det faktiskt ändrar HTML-innehållet, medan det andra alternativet ändrar dokumentformaten. Det här alternativet kan också ta bort stycken som infogades med calibres alternativ Infoga tom rad.

Se till att scenavbrytning är konsekvent formaterade

Med det här alternativet kommer calibre att försöka upptäcka vanliga scen-brytmarkörer och se till att de är centrerad. ”Mjuka” scenavbrytningsmarkörer, dvs. scenavbrytning definieras endast av extra tomrum, är utformade för att säkerställa att de inte kommer att visas i samband med sidbrytningar.

Ersätta scenbrytningar

Om det här alternativet är anpassat kommer calibre att ersätta scenbrytningsmarkörer som den hittar med den ersättningstext som användaren angett. Observera att vissa prydnadstecken kanske inte stöds på alla läsenheter.

I allmänhet bör du undvika att använda HTML-taggar, calibre kommer att bortse från alla taggar och använda fördefinierad markering. <hr /> taggar, dvs. horisontella regler och <img> taggar är undantag. Horisontella regler kan valfritt anges med format, om du väljer att lägga till ditt eget format måste du inkludera inställningen ”width”, annars kommer formatinformationen att kasseras. Bildtaggar kan användas, men calibre ger inte möjlighet att lägga till bilden under konvertering, detta måste göras efter att ha använt funktionen ”Redigera bok”.

Exempel bildtagg (placera bilden i en ”Images”-mapp inne i EPUB:en efter konvertering):

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

Exempel horisontell linje med format:

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

Ta bort onödiga bindestreck

calibre analyserar allt bindestreckinnehåll i dokumentet när detta alternativ är aktiverat. Själva dokumentet används som en ordbok för analys. Detta gör det möjligt för calibre att noggrant ta bort bindestreck för alla ord i dokumentet på vilket språk som helst, tillsammans med påhittade och dunkla vetenskapliga ord. Den främsta nackdelen är att ord som bara visas en gång i dokumentet inte kommer att ändras. Analys sker i två pass, det första passet analyserar radslut. Rader bryts endast om ordet finns med eller utan bindestreck i dokumentet. Det andra passet analyserar alla ord med bindestreck i hela dokumentet, bindestreck tas bort om ordet finns någon annanstans i dokumentet utan matchning.

Kursivera vanliga ord och mönster

När det är aktiverat kommer calibre att leta efter vanliga ord och mönster som betecknar kursiv stil och kursiverar dem. Exempel är vanliga textkonventioner som ~ord~ eller fraser som generellt bör kursiveras, t.ex. latinska fraser som ’etc.’ eller ’et cetera’.

Ersätta enhetsindrag med CSS-indrag

Vissa dokument använder en konvention för att definiera textindrag med icke-brytande blanksteg. När det här alternativet är aktiverat försöker calibre att upptäcka den här typen av formatering och konvertera dem till en 3% textindragning med CSS.

Söka & ersätta

De här alternativen är användbara främst för konvertering av PDF-dokument eller OCR-konverteringar, även om de också kan användas för att åtgärda många dokumentspecifika problem. Som ett exempel kan vissa konverteringar lämna efter sidhuvuden och sidfötter i texten. De här alternativen använder reguljära uttryck för att försöka upptäcka sidhuvuden, sidfötter eller annan godtycklig text och ta bort eller ersätta dem. Kom ihåg att de är verksamma på mellanliggande XHTML som produceras av konverteringsprocessen. Det finns en guide som hjälper dig att anpassa de reguljära uttrycken för ditt dokument. Klicka på trollstaven bredvid uttrycksrutan och klicka på knappen ”Testa” när du har skapat ditt sökuttryck. Framgångsrika träffar kommer att markeras i gult.

Sökningen fungerar med hjälp av ett Python-reguljärt uttryck. All matchad text tas helt enkelt bort från dokumentet eller ersätt med hjälp av ersättningsmönstret. Ersättningsmönster är valfritt, om det lämnas tomt kommer text som matchar sökbegreppet kommer att tas bort från dokumentet. Du kan läsa mer om reguljära uttryck och deras syntax på Allt om att använda reguljära uttryck i calibre.

Struktrurdetektering

Strukturdetektering innebär att calibre försöker sitt bästa för att upptäcka strukturella element i inmatningsdokumentet, när de inte är korrekt angivet. Till exempel kapitel, sidbrytningar, rubriker, sidfot osv. Som ni kan föreställa er, varierar denna process mycket från bok till bok. Lyckligtvis har calibre mycket kraftfulla alternativ för att hantera detta. Med makt kommer komplexitet, men om när du tar dig tid att lära sig komplexiteten, upptäcker du att det väl värt ansträngningen.

Kapitel och sidbrytningar
calibre har två uppsättningar av alternativ för kapiteldetektering och infoga sidbrytningar. Detta kan ibland vara något förvirrande, eftersom calibre som standard kommer att infoga sidbrytningar innan upptäckta kapitel såväl som platser som upptäcks av alternativet sidbrytningar. Anledningen till detta är att det ofta finns plats där sidbrytningar ska infogas som inte är kapitelgränser. Dessutom kan upptäckta kapitel eventuellt infogas i den automatiskt skapade innehållsförteckningen.

calibre använder XPath, ett kraftfullt språk för att göra det möjligt för användaren att specificera kapitelgränser/sidbrytningar. XPath kan verka lite skrämmande att använda till en början, lyckligtvis finns det en XPath-handledning i användarmanualen. Kom ihåg att strukturdetektering fungerar på den mellanliggande XHTML produceras av konverteringsprocessen. Använd felsökningsalternativet som beskrivs i Introduktion för att ta reda på lämpliga inställningar för din bok. Det finns också en knapp för en XPath-guide som hjälper till med skapande av enkla XPath-uttryck.

Som standard använder calibre följande uttrycker för att upptäcka kapitel:

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

Detta uttryck är ganska komplicerat, eftersom det försöker hantera ett antal gemensamma fall samtidigt. Vad det betyder är att calibre antar att kapitel börjar på antingen <h1> eller <h2> taggar som har något av orden (chapter, book, section eller part) i dem eller som har class = ”chapter” attribut.

Ett relaterat alternativ är Kapitelmarkering, som låter dig anpassa vad calibre gör när det upptäcker ett kapitel. Som standard kommer det att infoga en sidbrytning före kapitlet. Du kan låta den infoga en uppmätt rad istället för eller utöver sidbrytningen. Du kan också låta den göra ingenting.

Standardinställningen för att upptäcka sidbrytningar är:

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

vilket innebär att calibre kommer att infoga sidbrytningar före varje <h1> och <h2 tagg som standard.

Observera

Standarduttryck kan ändras beroende på inmatningsformat du konverterar.

Diverse

Det finns några fler alternativ i det här avsnittet.

Infoga metadata som en sida i början av boken

En av de bästa sakerna med calibre är att det tillåter dig att behålla mycket kompletta metadata om alla dina böcker, till exempel ett betyg, taggar, kommentarer osv. Det här alternativet skapar en enda sida med alla dessa metadata och infogar dem i den konverterade e-boken, vanligtvis precis efter omslaget. Se det som ett sätt att skapa egna anpassade bokomslag.

Ta bort första bilden

Ibland innehåller källdokumentet du konverterar omslaget som en del av boken, istället för som ett separat omslag. Om du också ange ett omslag i calibre, den då konverterade boken kommer att ha två omslag. Det här alternativet kommer att helt enkelt ta bort den första bilden från källdokumentet, vilket säkerställer att den konverterade boken har bara ett omslag, det som anges i calibre.

Innehållsförteckning

När inmatningsdokumentet har en innehållsförteckning i dess metadata, kommer calibre att använda det. Men ett antal äldre format stöder antingen inte en metadata baserade innehållsförteckning, eller så har inte enskilda dokument en. I dessa fall kan alternativen i det här avsnittet hjälp dig att automatiskt skapa en innehållsförteckning i den konverterade e-boken, baserat på det faktiska innehållet i inmatningsdokumentet.

Observera

Att använda dessa alternativ kan vara lite utmanande att få helt rätt. Om du föredrar att skapa/redigera innehållsförteckningen för hand, konvertera till EPUB- eller AZW3-formaten och välja kryssrutan längst ner i avsnittet innehållsförteckning i konverteringsdialogrutan som heter Finjustera innehållsförteckningen manuellt efter att konverteringen är klar. Detta kommer att starta redigeringsverktyget för innehållsförteckningar efter konverteringen. Det låter dig skapa poster i innehållsförteckningen genom att helt enkelt klicka på platsen i boken du vill att posten ska hänvisa till. Du kan också använda Redigerare för innehållsförteckningar för sig själv, utan att göra en konvertering. Gå till Inställningar → Gränssnitt → Verktygsfält och lägg till Redigerare för innehållsförteckningar i huvudverktygsfältet. Välj sedan bara boken du vill redigera och klicka på knappen Redigerare för innehållsförteckningar.

Det första alternativet är Tvinga användning av automatiskt skapad innehållsförteckning. Genom att markera det här alternativet kan du få calibre att åsidosätta innehållsförteckningar som finns i metadata för inmatningsdokumentet med den automatiskt skapade.

Det vanliga sättet skapandet av den automatiskt skapade innehållsförteckningen fungerar är att calibre först försöker att lägga till alla upptäckta kapitel till den skapade innehållsförteckningen. Du kan lära dig hur du anpassar upptäckten av kapitel i avsnittet Struktrurdetektering ovan. Om du inte vill inkludera upptäckta kapitel i den skapade innehållsförteckningen, se alternativet Lägg inte till upptäcka kapitel i innehållsförteckningen.

Om mindre kapitel än Kapiteltröskel upptäcktes kommer calibre att lägga till alla hyperlänkar som hittas i inmatningsdokumentet till innehållsförteckningen. Detta fungerar ofta bra: många inmatningsdokument inkluderar en hyperlänkad innehållsförteckning direkt i början. Alternativet Antal länkar kan användas för att anpassa detta beteende. Om värdet är noll läggs inga länkar till. Om det är inställt på ett tal som är större än noll läggs högst det antalet länkar till.

calibre kommer automatiskt att filtrera dubbletter från den skapade innehållsförteckningen. Men om det finns några ytterligare oönskade poster kan du filtrera dem med alternativet Innehållsförteckningsfilter. Detta är ett reguljärt uttryck som söker titeln på posterna i den skapade innehållsförteckningen. När en träff hittas kommer den att tas bort. Om du till exempel vill ta bort alla poster med titlarna ”Nästa” eller ”Föregående” använder du:

Next|Previous

Alternativen Nivå 1,2,3 innehållsförteckning låter dig skapa en sofistikerad innehållsförteckning på flera nivåer. De är XPath-uttryck som matchar taggar i den mellanliggande XHTML som produceras av konverteringsprocessen. Se Introduktion för hur du får tillgång till denna XHTML. Läs också XPath-handledning, för att lära dig hur man konstruerar XPath-uttryck. Bredvid varje alternativ finns en knapp som startar en guide som hjälper dig att skapa grundläggande XPath-uttryck. Följande enkla exempel illustrerar hur du använder de här alternativen.

Anta att du har en inmatningsdokument som resulterar i XHTML som ser ut så här:

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

Sedan anger vi alternativ som:

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

Detta kommer att resultera i en automatiskt skapad innehållsförteckning på två nivåer som ser ut som:

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

Varning

Inte alla utmatningsformat stöder en innehållsförteckning med flera nivåer. Du bör först försöka med EPUB-utmatning. Om det fungerar, testa sedan ditt val av format.

Använda bilder som kapitelrubriker vid konvertering av HTML-inmatningsdokument

Anta att du vill använda en bild som din kapitelrubrik, men ändå vill att calibre automatiskt ska kunna skapa en innehållsförteckning åt dig från kapitelrubrikerna. Använd följande HTML-markering för att uppnå detta:

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

Ställ in inställningen Nivå 1 innehållsförteckning till //h:h2. Då, i kapitel två, tar calibre titeln från värdet av attributet title på <h2>-taggen, eftersom taggen inte har någon text.

Använda taggattribut för att leverera texten för posterna i innehållsförteckningen

Om du har särskilt långa kapiteltitlar och vill ha förkortade versioner i innehållsförteckningen kan du använda attributet title för att uppnå detta, till exempel:

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

Ändra Nivå 1 TOC inställning till //h:h2/@titel. Då tar calibre titeln från värdet på titel attribut på <h2> taggar, istället för att använda texten i taggen. Observera den avslutande /@titel på XPath-uttryck kan du använda det här formuläret för att berätta calibre för att få text från alla attribut du vill.

Hur alternativ ställs in/sparas för konvertering

Det finns två ställen där konverteringsalternativ kan ställas in i calibre. Den första är i Inställningar->Konvertering. Dessa inställningar är standardinställningarna för konverteringsalternativen. Varje gång du försöker konvertera en ny bok kommer inställningarna här att användas som standard.

Du kan också ändra inställningar i konverteringsdialogrutan för varje bokkonvertering. När du konverterar en bok kommer calibre att komma ihåg inställningarna som du använde för den bok, så att om du konverterar den igen, kommer de sparade inställningarna för den enskilde boken att ha företräde framför standardinställningarna som anges i Inställningar. Du kan återställa de enskilda inställningarna till standardvärdena genom att använda knappen Återställ till standardvärden i enskilda bokkonverteringsdialogrutan. Du kan ta bort de sparade inställningarna för en grupp av böcker genom att välja alla böcker och sedan klicka på knappen Redigera metadata för att ta fram redigeringsdialogrutan för att massändra metadata, nära botten av dialogrutan är ett alternativ att ta bort lagrade konverteringsinställningar.

När du masskonverterar en uppsättning böcker, tas inställningarna i följande ordning (sista vinner):

- Från standardinställningarna i Inställningar->Konvertering

- Från de sparade konverteringsinställningarna för varje bok som konverteras (om någon). Detta kan stängas av alternativ i det övre vänstra hörnet av dialogrutan Masskonvertering.

- Från inställningarna i masskonverteringsdialogrutan

Observera att de slutliga inställningar för varje bok i en masskonvertering kommer att sparas och återanvändas om boken konverteras igen. Eftersom högsta prioritet av masskonvertering ges till inställningarna av dialogrutan Masskonvertering kommer dessa åsidosätta bokens specifika inställningar. Så du ska bara masskonvertera böcker tillsammans som behöver liknande inställningar. Undantaget är metadata och specifika inställningar för inmatningsformat. Eftersom dialogrutan Masskonvertering inte har inställningar för dessa två kategorier, kommer de att tas från boken specifika inställningar (om någon) eller standardvärdena.

Observera

Du kan se de faktiska inställningarna som används under en konvertering genom att klicka på den roterande ikonen i det nedre högra hörnet och sedan dubbelklicka på det enskilda konverteringsjobbet. Detta kommer att ta upp en konverteringslogg som innehåller de faktiska inställningarna som används, nära toppen.

Formatspecifika tips

Här hittar du tips som är specifika för konvertering av vissa format. Alternativ specifika för visst format, oavsett inmatning eller utmatning finns i dialogrutan konvertering under eget avsnitt, till exempel TXT-inmatning eller EPUB-utmatning.

Konvertera Microsoft Word-dokument
calibre kan automatiskt konvertera .docx-filer som skapats av Microsoft Word 2007 och nyare. Lägg bara till filen i calibre och klicka på konvertera.

Observera

Det finns en demo .docx-fil [https://calibre-ebook.com/downloads/demos/demo.docx] som demonstrerar förmågorna hos calibres konverteringsmotor. Bara hämta den och konvertera den till EPUB eller AZW3 för att se vad calibre kan göra.

calibre kommer att automatiskt skapa en innehållsförteckning som baseras på rubriker om du markerar dina rubriker med formaten Heading 1, `Heading 2` osv. i Microsoft Word. Öppna utmatnings-e-boken i calibres e-bokvisare och klicka på knappen Innehållsförteckning för att visa den skapade innehållsförteckningen.

Äldre .doc-filer

För äldre .doc-filer kan du spara dokumentet som HTML med Microsoft Word och sedan konvertera den resulterande HTML-filen med calibre. När du sparar som HTML, se till att använda alternativet ”Spara som webbplats, filtrerad” eftersom detta kommer att producera ren HTML som kommer att konverteras bra. Observera att Word producerar en riktigt rörig HTML, att konvertera den kan ta lång tid, så ha tålamod. Om du har en nyare version av Word tillgänglig kan du också spara den direkt som .docx.

Ett annat alternativ är att använda gratis LibreOffice. Öppna din .doc-fil i LibreOffice och spara den som .docx, som direkt kan konverteras i calibre.

Konvertera TXT-dokument

TXT-dokument har inget väl definierat sätt att specificera formatering som fetstil, kursiv, osv., eller dokumentstruktur som stycken, rubriker, avsnitt och så vidare, men det finns en mängd konventioner som vanligtvis används. Som standard försöker calibre automatisk upptäcka korrekt formatering och markering baserat på dessa konventioner.

TXT-inmatning stöder ett antal alternativ för att differentiera hur stycken upptäcks.

Styckeformat: automatisk

Analyserar textfilen och försök att automatiskt avgöra hur styckena är definierade. Detta val kommer i allmänhet fungerar bra, om du uppnår oönskade resultat testa ett av de manuella alternativen.

Styckeformat: blockera

Förutsätter en eller flera tomma rader är en styckesgräns:

This is the first.

This is the
second paragraph.

Styckeformat: ensamstående

Förutsätter att varje rad är ett stycke:

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

Styckeformat: skriv ut

Förutsätter att varje stycke börjar med en indragning (antingen en tabulering eller 2+ blanksteg). Styckena avslutas när nästa rad som börjar med en indragning nås:

This is the
first.
This is the second.

This is the
third.

Styckeformat: oformaterad

Förutsätter att dokumentet inte har någon formatering, men använder hårda radbrytningar. Skiljetecken och medianradslängd används för att försöka återskapa stycken.

Formateringsformat: automatisk

Försöker hitta den typ av formateringsmarkering som används. Om ingen markering används kommer heuristisk formatering att tillämpas.

Formateringsformat: heuristisk

Analyserar dokumentet för vanliga kapitelrubriker, scenbrytningar och kursiverade ord och tillämpar lämplig HTML-markering under konvertering.

Formateringsformat: Markdown

calibre stöder också körning av TXT-inmatning genom en omvandlingsförbehandlare känd som Markdown. Markdown gör att grundläggande formatering kan läggas till i TXT-dokument, såsom fetstil, kursiv stil, avsnittsrubriker, tabeller, listor, en innehållsförteckning osv. Markera kapitelrubriker med en ledande # och ställ in XPath-kapiteldetekteringsuttrycket till ”//h:h1” är det enklaste sättet att skapa en korrekt innehållsförteckning från ett TXT-dokument. Du kan läsa mer om Markdown-syntaxen på daringfireball [https://daringfireball.net/projects/markdown/syntax].

Formateringsformat: ingen

Använder ingen speciell formatering av texten, dokumentet konverteras till HTML utan några ändringar.

Konvertera PDF-dokument

PDF-dokument är en av de värsta format att konvertera från. De är ett fast sidstorleks- och textplaceringsformat. Det betyder att det är mycket svårt att avgöra var ett stycke slutar och ett annat börjar. calibre kommer att försöka dela upp stycken med en anpassningsbar Radbrytningsfaktor. Detta är en skala som används för att bestämma längden för var en rad ska radbrytas. Giltiga värden är ett decimaltal mellan 0 och 1. Standard är 0,45, precis under medianradslängden. Sänk detta värde för att inkludera mer text i radbrytningen. Öka för att inkludera mindre. Du kan justera detta värde i konverteringsinställningarna under PDF-inmatning.

Dessutom har de ofta sidhuvuden och sidfötter som en del av dokumentet som kommer att bli inkluderat med texten. Använd panelen Sök och ersätt för att ta bort sidhuvuden och sidfötter för att mildra problemet. Om sidhuvuden och sidfötterna inte tas bort från texten kan kasta bort avsnittsuppdelning. För att lära dig hur du tar bort alternativ för sidhuvud och sidfot, läs Allt om att använda reguljära uttryck i calibre.

Vissa begränsningar på PDF-inmatning är:

- Komplexa, flerkolumn- och bildbaserade dokument stöds inte.

- Extrahering av vektorbilder och tabeller från inuti dokumentet stöds inte heller.

- Vissa PDF-filer använder speciella glyfer för att representera ll eller ff eller fi osv. Konvertering av dessa kanske eller kanske inte fungerar beroende på hur de representeras internt i PDF:en.

- Länkar och innehållsförteckning stöds inte

- PDF-filer med inbäddade icke-Unicode-teckensnitt för att representera icke-engelska tecken kommer att resultera i förvrängt utmatning för dessa tecken

- Vissa PDF-filer är uppbyggda av fotografier av sidan med OCR-text bakom dem. I sådana fall använder calibre OCR-texten, vilket kan vara mycket annorlunda från vad du ser när du visar PDF-filen

- PDF-filer som används för att visa komplex text, som höger till vänster språk och matematisk typsättning kommer inte att konverteras korrekt

För att återiterera är PDF ett riktigt, riktigt dålig format att använda som inmatning. Om du absolut måste använda PDF, var beredd på en utmatning som sträcker sig allt från anständig till oanvändbar, beroende på den inmatade PDF-filen.

Serietidningssamlingar

En serietidningssamling är en .cbc-fil. En .cbc-fil är en ZIP-fil som innehåller andra CBZ-/CBR-filer. Dessutom måste .cbc-filen innehålla en enkel textfil som heter comics.txt, kodade i UTF-8. comics.txt-filen måste innehålla en förteckning över de comics-filerna inuti .cbc-filen i formen filename:title, enligt nedan:

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

.cbc-filen kommer då innehålla:

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

calibre kommer automatiskt konvertera denna .cbc-fil till en e-bok med en innehållsförteckning som hänvisar till varje uppslagsord i serietidning.txt.

Demonstration av avancerad EPUB-formatering

Olika avancerad formatering för EPUB-filer demonstreras i den här demofilen [https://calibre-ebook.com/downloads/demos/demo.epub]. Filen skapades från handkodad HTML med calibre och är tänkt att användas som en mall för dina egna EPUB skapande insatser.

HTML-källan som den skapades från är tillgänglig demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. Inställningarna som används för att skapa EPUB:en från ZIP-filen är:

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

Observera att eftersom den här filen utforskar potentialen i EPUB, kommer inte de flesta av de avancerade formatering att fungera på läsenheter mindre kapabla än calibres inbyggda EPUB-visare.

Konvertera ODT-dokument
calibre kan direkt konvertera ODT (Opendocument Text)-filer. Du bör använda format för att formatera ditt dokument och minimera användningen av direkt formatering. När bilder infogas i ditt dokument måste du förankra dem till stycke, förankrade bilder till en sida kommer alla hamna på framsidan av konverteringen.

För att möjliggöra automatisk identifiering av kapitel måste du markera dem med de inbyggda formaten som heter Rubrik 1, Rubrik 2, …, Rubrik 16 (Rubrik 1 motsvarar HTML-taggen <h1>, Rubrik 2 till <h2> osv.). När du konverterar i calibre kan du ange vilket format du använde i Identifiera kapitel i rutan. Exempel:

- Om du markerar kapitel med formatet Rubrik 2 måste du ställa in rutan ”Upptäck kapitel i” till //h:h2

- För en kapslad innehållsförteckning med avsnitt markerad med Rubrik 2 och de kapitel som är markerade med Rubrik 3 måste du ange //h:h2|//h:h3. I konvertera - innehållsförteckningssidan ställ in rutan Nivå 1 Innehållsförteckning till //h:h2 och rutan nivå 2 innehållsförteckning till //h:h3.

Välkända dokumentegenskaper (titel, sökord, beskrivning, skapare) känns igen och calibre kommer att använda den första bilden (inte för liten, och med goda proportioner) som omslagsbild.

Det finns också ett avancerat egenskapskonverteringsläge, som aktiveras genom att ställa in den anpassade egenskapen opf.metadata (av ”Ja eller Nej” typ) till Ja i ditt ODT-dokument (Arkiv->Egenskaper->Anpassa egenskaper). Om den här egenskapen upptäcks av calibre känns följande anpassade egenskaper igen (opf.authors åsidosätter dokumentets skapare):

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

Utöver detta kan du ange vilken bild som ska användas som omslaget genom att namnge den opf.cover (högerklicka, Bild-> Alternativ-> Namn) i ODT. Om ingen bild med detta namn hittas, används den ”smarta” metoden. Eftersom omslagsupptäckt kan leda till dubbla omslag i vissa utmatningsformat, kommer processen att ta bort stycken (endast om det enda innehållet är omslaget!) från dokumentet. Men detta fungerar bara med namngiven bild!

För att inaktivera omslagsupptäckt kan du ställa in den anpassade egenskapen opf.nocover (’Ja eller Nej’-typ) till Ja i avancerat läge.

Konvertera till PDF

Den första, viktigaste inställningen att bestämma när man konverterar till PDF är sidstorleken. Som standard använder calibre en sidstorlek för ”U.S. Letter”. Du kan ändra detta till en annan standard sidstorlek eller en helt anpassad storlek i avsnittet PDF-utmatning sektionen i konverteringsdialogrutan. Om du skapar en PDF som ska användas på en viss enhet kan du slå på alternativet för att använda sidstorleken från utmatningsprofilen istället. Så om din utskriftsprofil är inställd på Kindle, kommer calibre att skapa en PDF med sidstorlek som är lämplig för visning på den lilla Kindle-skärmen.

Sidhuvuden och sidfötter

Du kan infoga godtyckliga sidhuvud och sidfot på varje sida i PDF-filen genom att ange sidhuvuds- och sidfotsmallar. Mallar är bara kodavsnitt av HTML-kod som blir återgivna i sidhuvuds- och sidfotsplatser. Till exempel för att visa sidnummer centrerade längst ner på varje sida, i grönt, använd följande sidfotsmall:

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

calibre ersätter automatiskt _PAGENUM_ med det aktuella sidnumret. Du kan även sätta olika innehåll på jämna och udda sidor, till exempel följande rubrikmall visar titeln på udda sidor och författaren på jämna sidor:

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

calibre kommer automatiskt ersätta _TITLE_ och _AUTHOR_ med titeln och författaren till dokumentet konverteras. Inställningen justify-content till flex-end kommer att leda till att texten är rätt justerad.

Du kan också visa text på vänster- och högerkant och ändra teckensnittsstorleken, vilket framgår av den här sidhuvudmallen:

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

Detta kommer att visa titeln till vänster och författare till höger, i en teckensnittsstorlek som är mindre än huvudtexten.

Du kan också använda det aktuella avsnittet i mallar, enligt nedan:

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

_SECTION_ Ersätts med oavsett vad namnet på det aktuella avsnittet är. Dessa namn är tagna från metadata Innehåll i dokumentet (PDF Outline). Om dokumentet inte har någon innehållsförteckning då det kommer att ersättas av tom text. Om en enda PDF-sida har flera avsnitt, kommer det första avsnittet på sidan användas. Likaså finns det en variabel med namnet _TOP_LEVEL_SECTION_ som kan användas för att ge namnet på det nuvarande toppnivåavsnittet.

Du kan till och med använda JavaScript i sidhuvud- och sidfotsmallarna, till exempel kommer följande mall att leda till att sidnummer börjar vid 4 istället för 1:

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

Dessutom finns det några fler variabler du kan använda i sidhuvud och sidfot, dokumenterade nedan:

- _TOTAL_PAGES_ - totalt antal sidor i PDF-filen, till exempel användbar för att implementera en framstegsräknare.

- _TOP_LEVEL_SECTION_PAGES_ - totalt antal sidor i det aktuella toppnivåavsnittet

- _TOP_LEVEL_SECTION_PAGENUM_ - sidnumret för den aktuella sidan inom det aktuella toppnivåavsnittet

Observera

När du lägger till sidhuvud och sidfot, se till att du ställer in sidans övre och nedre marginaler till tillräckligt stora värden under avsnittet PDF-utmatningsformat i konverteringsdialogrutan.

Utskrivbar innehållsförteckning

Du kan också infoga en utskrift/tryckbar innehållsförteckning i slutet av PDF som listar sidnummer för varje avsnitt. Detta är mycket användbart om du tänker skriva ut PDF till papper. Om du vill använda PDF på en elektronisk produkt, ger PDF Outline denna funktionalitet och är skapad som standard.

Du kan anpassa utseendet på den skapade innehållsförteckningen genom att använda extra CSS-konverteringsinställningen under Utseende & känsla-delen i konverteringsdialogrutan. Standard CSS som används är listade nedan, helt enkelt kopiera den och gör de ändringar du vill.

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

Anpassade sidmarginaler för enskilda HTML-filer

Om du konverterar en EPUB- eller AZW3-fil med flera enskilda HTML-filer inuti den och du vill ändra sidmarginalerna för en viss HTML-fil kan du lägga till följande formatblock i HTML-filen med calibre e-bokredigeraren:

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

Aktivera sedan alternativet Använd sidmarginaler från dokumentet som konverteras i PDF-utmatningsavsnittet av konverteringsdialogrutan. Nu kommer alla sidor som skapas från denna HTML-fil att ha 10pt marginaler.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Redigera e-böcker

Redigera e-böcker
calibre har en integrerad e-bokredigerare som kan användas för redigera böcker i EPUB- och AZW3 (Kindle)-format. Redigeraren visar dig HTML och CSS som används internt i bokfilerna, med en realtidsförhandsvisning som uppdateras allt eftersom du gör ändringar. Den innehåller också olika automatiserade verktyg för att utföra vanliga sanerings- och justeringsuppgifter.

Du kan använda denna redigerare genom att högerklicka på valfri bok i calibre och välja Redigera bok.

Innehåll

- Grundläggande arbetsflöde

- Filhanteraren

- Byta namn på filer

- Slå samman filer

- Ändra textfilordning

- Markera omslaget

- Ta bort filer

- Exportera filer

- Lägga till nya bilder/teckensnitt/osv. eller skapa nya tomma filer

- Ersätta filer

- Länka formatmallar till HTML-filer effektivt

- Söka & ersätta

- Sparade sökningar

- Funktionsläge

- Sök ignorerar HTML-taggar

- Automatiserade verktyg

- Redigera innehållsförteckningen

- Kontrollera boken

- Lägga till ett omslag

- Bädda in refererade teckensnitt

- Underuppsätta inbäddade teckensnitt

- Förbättra skiljeteckenhantering

- Omvandla CSS-egenskaper

- Ta bort oanvända CSS-regler

- Justera HTML

- Försköna filer

- Infoga en indragsinnehållsförteckning

- Ställa in semantik

- Filtrera formatinformation

- Uppgraderar bokens inre delar

- Kontrollpunkter

- Panelen för realtidsförhandsvisning

- Dela upp HTML-filer

- CSS-realtidspanelen

- Diverse verktyg

- Innehållsförteckningsvyn

- Kontrollera stavning av ord i boken

- Infoga specialtecken

- Kodinspektörsvyn

- Kontrollera externa länkar

- Hämta externa resurser

- Ordna filer i mappar efter typ

- Importera filer i andra e-bokformat som EPUB

- Rapportverktyget

- Speciella funktioner i kodredigeraren

- Syntaxmarkering

- Kontextkänslig hjälp

- Automatisk komplettering

- Kodavsnitt

Grundläggande arbetsflöde

Observera

En videoklippsrundtur i calibre e-bokredigeraren finns här [https://calibre-ebook.com/sv/demo#tutorials].

När du först öppnar en bok med verktyget Redigera bok kommer få se en list med filer till vänster. Det här är enskilda HTML-filer, formatmallar, bilder osv. som tillsammans utgör innehållet av boken. Dubbelklicka helt enkelt på en fil för att börja redigera den. Observera att om du vill göra något mer sofistikerat än små justeringar, behöver du känna till HTML-handledningen [http://html.net/tutorials/html/] och CSS-handledningen [http://html.net/tutorials/css/].

Allt eftersom du gör ändringar i HTML eller CSS i redigeraren kommer ändringarna att förhandsvisas i realtid i förhandsvisningspanelen till höger. När du är nöjd med hur ändringarna du har gjort ser ut, klicka på knappen Spara eller använd Arkiv → Spara för att spara dina ändringar i e-boken.

En användbar funktion är Kontrollpunkter. Innan du börjar en ambitiös uppsättning redigeringar kan du skapa en kontrollpunkt. Kontrollpunkten kommer att bevara det aktuella tillståndet för din bok, så om du i framtiden bestämmer du inte gillar de ändringar du har gjort kan gå tillbaka till tillståndet när du skapade kontrollpunkten. För att skapa en kontrollpunkt, använd Redigera → Skapa kontrollpunkt. Kontrollpunkter skapas automatiskt för dig när du kör något automatiserat verktyg som global sökning och ersättning. Den kontrollpunktsfunktionaliteten är utöver det normala ångra/göra om-mekanismen vid redigering av enskilda filer. Kontrollpunkter behövs när ändringar är spridda över flera filer i boken.

Det är det grundläggande arbetsflödet för att redigera böcker – Öppna en fil, gör ändringar, förhandsgranska och spara. Resten av den här manualen kommer att diskutera de olika verktygen och funktionerna som finns för att du ska kunna utföra specifika uppgifter effektivt.

Filhanteraren

Filhanteraren ger dig en översikt över de olika filerna inne i boken som du redigerar. Filerna är ordnade efter kategori, med textfiler (HTML) överst, följt av formatmallsfiler (CSS), bilder och så vidare. Dubbelklicka helt enkelt på en fil för att börja redigera den. Redigering stöds för HTML-, CSS- och bildfiler. Ordningen på textfiler är samma ordning som de skulle visas i om du läser boken. Alla andra filer är ordnade i alfabetisk ordning.

Genom att dra din mus över en post, kan du se dess storlek, men också, längst ner på skärmen, den fullständiga sökvägen till filen inne i boken. Observera att filer i e-böcker är komprimerade, så storleken på den sista boken är inte summan av de enskilda filstorlekar.

Många filer har speciell betydelse, i boken. Dessa har normalt en ikon bredvid deras namn, med angivande av speciell betydelse. Till exempel på bilden till vänster, kan du se att filerna cover_image.jpg och titlepage.xhtml som har ikonen av ett omslag bredvid dem, indikerar detta att de är bokomslagsbilden och titelsida. På liknande sätt har content.opf`-filen en metadata ikon bredvid sig, vilket indikerar att bokens metadata finns i den och toc.ncx-filen har en T ikon bredvid sig, vilket indikerar att det är innehållsförteckningen.

Du kan utföra många åtgärder på enskilda filer genom att högerklicka på dem.

Byta namn på filer

Du kan byta namn på en enskild fil genom att högerklicka på den och välja Byt namn. Namnbytningen av en fil uppdaterar automatiskt alla länkar och referenser till den i hela boken. Så allt du behöver göra är att ge det nya namnet, calibre kommer att ta hand om resten.

Du kan också byta namn på många filer samtidigt. Detta är användbart om du vill att filerna ska ha ett enkelt namnmönster. Till exempel kanske du vill byta namn på alla HTML-filer för att ha namn Kapitel-1.html, Kapitel-2.html och så vidare. Välj filerna du vill byta namn på genom att hålla ner Skift- eller Ctrl-tangenten och klicka på filerna. Högerklicka sedan och välj Massändring av namn. Ange ett prefix och vilket nummer du vill att den automatiska numreringen ska börja på, klicka på OK så är du klar. I dialogrutan flernamnbyte kan du också byta namn på filer efter den ordning de visas i boken istället för den ordning du valde dem i, användbart, till exempel för att byta namn på alla bilder efter den ordning de visas.

Slutligen, du kan massändra filändelsen för alla markerade filer. Markera flera filer, som ovan, och högerklicka och välj Ändra filändelsen för den valda files.

Slå samman filer

Ibland kanske du vill slå samman två HTML-filer eller två CSS-filer tillsammans. Det kan ibland vara bra att ha allt i en enda fil. Var försiktig dock, att sätta en hel del innehåll i en enda fil orsakar prestandaproblem när du tittar på boken i en typisk e-bokläsare.

För att slå samman flera filer, välj dem genom att hålla ned Ctrl-tangenten och klicka på dem (se till att du bara välja filer av en typ, antingen alla HTML-filer eller alla CSS-filer och så vidare). Högerklicka sedan och välj slå samman. Det är allt, calibre kommer att slå samman filerna, ta hand om att migrera alla länkar och referenser automatiskt till de sammanslagna filerna. Observera att sammanslagning av filer ibland kan göra att textformateringen ändras, eftersom de enskilda filerna kunde ha använt olika formatmallar.

Du kan också välja textfiler och sedan dra och släppa textfilerna till en annan textfil för att slå samman de släppta textfilerna till måltextfilen.

Ändra textfilordning

Du kan ändra ordningen i vilka textfiler (HTML) öppnas när du läser boken genom att helt enkelt dra och släppa dem i filhanteraren eller klicka på filen för att flytta och sedan trycka på Ctrl+Skift-modifierarna med tangenterna Up, Down, Home eller End. För den tekniskt benägna kallas detta att ordna om bokryggen.

Observera att du måste släppa posterna mellan andra poster, inte ovanpå dem, detta kan vara lite krångligt tills du vänjer dig vid det. Om du släpper ovanpå en annan fil kommer filerna att slås samman.

Markera omslaget

E-böcker har normalt en omslagsbild. Bilden visas i Filbläddraren med ikonen av en brun bok bredvid bildnamnet. Om du vill ange en annan bild som omslag, kan du göra det genom att högerklicka på filen och välja Välj som omslag.

Dessutom har EPUB-filer konceptet av en titelsida. En titelsida är en HTML-fil som fungerar som titelsida/omslag för boken. Du kan markera en HTML-fil som titelsida när du redigerar EPUB:er genom att högerklicka. Var noga med att filen du markerar endast innehåller omslagsinformationen. Om den innehåller annat innehåll, till exempel det första kapitlet, kommer det innehållet att gå förlorat om användaren någonsin konverterar EPUB-filen i calibre till ett annat format. Detta beror på att calibre vid konvertering antar att den markerade titelsidan endast innehåller omslaget och inget annat innehåll.

Ta bort filer

Du kan ta bort filer genom att antingen högerklicka på dem eller genom att välja dem och trycka på Delete tangenten. Ta bort en fil tar bort alla hänvisningar till filen från OPF-filen, sparar dig detta grovgöra. Men referenser på andra platser tas inte bort, du kan använda ”Kontrollera bok” verktyg för att enkelt hitta och ta bort/ersätta dem.

Exportera filer

Du kan exportera en fil från boken till någon annanstans på din dator. Detta är användbart om du vill arbeta på filen i sig, med specialverktyg. För att göra detta, helt enkelt högerklicka på filen och välj Exportera.

När du är klar arbetar på den exporterade filen kan du importera den i boken, genom att högerklicka på filen igen och välja Ersätt med fil… vilket gör att du kan ersätta filen i boken med den tidigare exporterad fil.

Du kan också kopiera filer mellan flera redigeringsinstanser. Välj de filer du vill kopiera i filhanteraren, högerklicka och välj, Kopiera valda filer till en annan redigeringsinstans. Sedan, i den andra redigeringsinstansen, högerklicka i filhanteraren och välj Klistra in filen från annan redigeringsinstans.

Lägga till nya bilder/teckensnitt/osv. eller skapa nya tomma filer

Du kan lägga till en ny bild, teckensnitt, formatmall osv. från din dator in i boken genom att klicka på Arkiv → Ny fil. Det gör att du antingen importera en fil genom att klicka på knappen Importera resursfil eller skapa en ny tom HTML-fil eller formatmall genom att helt enkelt skriva in filnamnet i rutan för den nya filen.

Du kan även importera flera filer till boken på en gång genom att använda Arkiv->Importera filer till bok.

Ersätta filer

Du kan enkelt ersätta befintliga filer i boken, genom att högerklicka på filen och välja ersätta. Detta kommer automatiskt att uppdatera alla länkar och referenser, om ersättningsfilen har ett annat namn än filen som ersätts.

Länka formatmallar till HTML-filer effektivt

Som en bekvämlighet kan du välja flera HTML-filer i filhanteraren, högerklicka och välj Länka formatmallar för att ha calibre automatiskt infoga <link> taggar för dessa formatmallar i alla markerade HTML-filer.

Söka & ersätta

Redigera bok har en mycket kraftfull sök- och ersättsgränssnitt som låter dig söka och ersätta text i den aktuella filen, över alla filer och till och med i ett markerat område av den aktuella filen. Du kan söka med hjälp av en normal sökning eller använda reguljära uttryck. För att lära dig att använda reguljära uttryck för avancerad sökning, se Allt om att använda reguljära uttryck i calibre.

Starta sök och ersätt via menyposten Sök → Sök/ersätt (du måste redigera en HTML- eller CSS-fil).

Fyll i texten du vill söka efter i rutan Sök och dess ersättning i rutan Ersätt. Du kan klicka på lämpliga knappar för att hitta nästa träff, ersätt den aktuella träffen och ersätt alla träffar.

Använda rullgardinsmenyer i botten av lådan, kan du söka arbeta över den aktuella filen, alla textfiler, alla formatfiler eller alla filer. Du kan också välja sökläget för att vara en normal (sträng) sökning eller ett reguljärt uttryckssökning.

Du kan räkna alla träffar för ett sökuttryck via Sök → Räkna alla. Räkningen kommer att köra över vilka filer/regioner som du har valt i rullgardinsmenyn.

Du kan också gå till en specifik rad i den öppna redigeraren via Sök → Gå till rad.

Observera

Kom ihåg att du måste använda reguljära uttryck för att utnyttja fulla kraften i sök och ersätt. Se Allt om att använda reguljära uttryck i calibre.

Sparade sökningar

Du kan spara ofta använda sök-/ersättuttryck (inklusive funktionslägesuttryck) och återanvända dem flera gånger. Om du vill spara en sökning högerklicka helt enkelt i rutan Spara aktuell sökning.

Du kan ta fram de sparade sökningarna via Sök → Sparade sökningar. Det här kommer att visa dig en lista med sök- och ersättuttryck som du kan använda. Du kan till och med välja flera poster i listan genom att hålla ner Ctrl-tangenten medan du klickar för att köra flera sök- och ersättuttryck i en enda operation.

Funktionsläge

Med funktionsläget kan du skriva godtyckliga och starka Python-funktioner som körs på varje Hitta/ersätt. Du kan göra i stort sett alla texthantering du vill i funktionsläge. För mer information se Funktionsläge för Sök & ersätt i Redigeraren.

Sök ignorerar HTML-taggar

Det finns också en dedikerat verktyg för att söka efter text och ignorera alla HTML-taggar däremellan. Till exempel om boken har HTML Empahisis på ett <i>ord</i>. Kan du söka efter på ett ord och det kommer att hittas även om det finns en <i>-tagg i mitten. Använd detta verktyg via menyalternativet Sök → Sök och ignorera HTML-markering.

Automatiserade verktyg

Redigera bok har olika verktyg för att hjälpa till med vanliga uppgifter. Dessa nås via menyn Verktyg.

Redigera innehållsförteckningen

Det finns en dedikerat verktyg för att underlätta redigering av innehållsförteckningen. Starta det med Verktyg → Innehållsförteckning → Redigera innehållsförteckning.

Verktyget Redigera innehållsförteckning visar den aktuella innehållsförteckningen (om någon) till vänster. Helt enkelt dubbelklicka på en post för att ändra texten. Du kan också ordna om poster genom att dra och släppa eller genom att använda knapparna till höger.

För böcker som inte har en befintlig innehållsförteckning, ger verktyget dig olika alternativ för att automatiskt skapa en innehållsförteckning från texten. Du kan skapa från rubrikerna i dokumentet, från länkar, från enskilda filer och så vidare.

Du kan redigera enskilda poster genom att klicka på dem och sedan klicka på knappen Ändra platsen denna startpunkt hänvisar till. Detta öppnar en miniförhandsvisning av boken, flytta helt enkelt muspekaren över bokvypanelen och klicka där du vill att posten ska hänvisa till. En tjock grön linje visar dig platsen. Klicka på OK när du är nöjd med platsen.

Kontrollera boken

Verktyget Kontrollera bok söker igenom din bok efter problem som kan förhindra det fungerar som avsett på faktiska läsaranordningar. Aktivera det via Verktyg → Kontrollera bok.

Eventuella problem som finns redovisas i en trevlig, lätt att använda listan. Genom att klicka på en post i listan visas lite hjälp om felet samt ger dig möjlighet att automatiskt åtgärda detta fel, om felet kan åtgärdas automatiskt. Du kan också dubbelklicka på felet för att öppna platsen för fel i en redigerare, så du kan åtgärda det själv.

Några av de kontroller som utförts är:

- Felaktig HTML-markering. Alla HTML-markeringar som inte analyseras som välformade XML rapporteras. Att korrigera det kommer att säkerställa att din markering fungerar som avsett i alla sammanhang. calibre kan också rätta dessa fel automatiskt, men automatisk rättning kan ibland ha oväntade effekter, så använd försiktigt. Som alltid skapas en kontrollpunkt före automatisk rättning så att du enkelt kan återställa alla ändringar. Automatisk rättning fungerar genom att analysera markeringen med HTML5-algoritmen, som är mycket feltolerant och sedan konverteras till välformade XML.

- Felaktiga eller okända CSS-format. Alla CSS som inte är giltig eller som har egenskaper som inte är definierade i standarden CSS 2.1 (plus några från CSS 3) rapporteras. CSS kontrolleras i alla formatmallar, indragsformatattribut på raden och <style> taggar i HTML-filer.

- Trasiga länkar. Länkar som hänvisar till filer inuti boken som saknas är rapporterade.

- Orefererade filer. Filer i boken som inte refereras av någon annan fil eller som inte finns i bokryggraden rapporteras.

- Olika vanliga problem i OPF-filer såsom dubbletter av bokryggar eller manifestposter, trasiga idrefs eller metaomslagstaggar, saknade nödvändiga avsnitt och så vidare.

- Olika kompatibilitetskontroller för kända problem som kan göra att boken inte fungerar på läsenheter.

Lägga till ett omslag

Du kan enkelt lägga till ett omslag till boken via Verktyg → Lägg till omslag. Detta gör att du antingen kan välja en befintlig bild i boken som omslag eller importera en ny bild in i boken och göra den till omslag. När du redigerar EPUB-filer är HTML-höljet för omslaget automatiskt skapat. Om ett befintligt omslag i boken hittas, ersätts det. Verktyget tar också automatiskt hand om korrekt markering av omslagsfilerna som omslag i OPF.

Bädda in refererade teckensnitt

Nås via Verktyg → Bädda in refererade teckensnitt, det här verktyget hittar alla teckensnitt som det refereras till i boken och om de inte redan är inbäddade, söker din dator efter dem och bäddar in dem i boken, om de hittas. Se till att du har nödvändiga upphovsrättar för att bädda in kommersiellt licensierade teckensnitt innan du gör detta.

Underuppsätta inbäddade teckensnitt

Nås via Verktyg → Använd delmängd av inbäddade teckensnitt, reducerar detta verktyg alla teckensnitt i boken att bara innehålla tecken för texten som faktiskt finns i boken. Detta minskar ofta storleken på teckensnittsfiler med ~ 50%. Dock var medveten om att när teckensnitten är en delmängd, om du lägger till ny text vars tecken är inte tidigare förekommer i den undergrupp teckensnitt, kommer teckensnittet inte att fungera för den nya texten. Så gör det bara som detta som sista steget i ditt arbetsflöde.

Förbättra skiljeteckenhantering

Konvertera bindestreck, ellipser, citattecken, flera bindestreck osv. till deras typografiskt korrekta motsvarigheter. Observera att algoritmen ibland kan skapa felaktiga resultat, särskilt när enkla citattecken i början av sammandragningar är inblandade. Nås via Verktyg → Förbättra skiljeteckenhantering.

Omvandla CSS-egenskaper

Skapa regler för att omvandla den grafiska utformningen av boken. Till exempel skapa en regel för att konvertera alla röd text till grön eller att fördubbla teckensnittsstorleken för all text i boken eller göra texten av en viss teckensnittsfamilj kursiv osv.

Att skapa regler är enkelt, reglerna följer en naturlig språkformat, som ser ut så här:

- Om egenskapen färg är röd ändra den till grön

- Om egenskapen teckensnittsstorlek är godtyckligt värde multiplicera värdet med 2

Tillgängligt via Verktyg → Omvandla format.

Ta bort oanvända CSS-regler

Ta bort alla oanvända CSS-regler från formatmallar och <style> taggar. Några böcker som skapas från produktionsmallar kan ha ett stort antal extra CSS-regler som inte matchar något materiellt innehåll. Dessa extra regler kan bromsa läsenheter som behöver bearbeta dem alla. Nås via Verktyg → Ta bort oanvända CSS.

Justera HTML

Detta verktyg konverterar HTML som inte kan analyseras som XML till välformad XML. Det är mycket vanligt i böcker för att ha icke-välformad XML, så detta verktyg automatiserar helt enkelt processen med att fastställa en sådan HTML. Verktyget fungerar genom att analysera HTML med hjälp av HTML5-algoritmen (den algoritm som används i alla moderna webbläsare) och sedan konvertera resultatet till XML. Var medveten om att auto-rättning ibland kan ha kontraintuitivt resultat. Om du vill kan du använda verktyget Kontrollera bok som diskuteras ovan för att hitta och manuellt korrigera problem i HTML. Nås via Verktyg → Reparera HTML.

Försköna filer

Det här verktyget används för att automatiskt formatera alla HTML- och CSS-filer så att de ”ser fina ut”. Koden är automatiskt indragen så raderna ser fina ut, tomma rader infogas vid behov och så vidare. Observera att försköna också automatiskt reparerar trasiga HTML/CSS. Om du inte vill att någon automatisk korrigering ska utföras, använd först Verktyget Kontrollera bok för att rätta till alla problem och kör sedan försköna. Åtkomst via Verktyg → Försköna alla filer.

Observera

I HTML kan valfri text med betydande mängd whitespace, via CSS-white-space-direktivet. Därför kan försköning potentiellt ändra återgivningen av HTML. För att undvika detta så långt det är möjligt förskönar försköningsalgoritmen bara blocknivåtaggar som innehåller andra blocknivåtaggar. Så till exempel kommer texten i en <p>-tagg inte att ha dess whitespace ändrade. Men en <body>-tagg som bara innehåller andra <p>- och <div>-taggar kommer att förskönas. Detta kan ibland innebära att en viss fil inte kommer att påverkas av försköningen eftersom den inte har några lämpliga blocknivåtaggar. I sådana fall kan du testa olika försköningsverktyg som är mindre försiktiga, till exempel: HTML Tidy [https://infohound.net/tidy/].

Infoga en indragsinnehållsförteckning

Normalt i e-böcker, är innehållsförteckningen åtskild från huvudtexten och nås vanligtvis via en speciell innehållsförteckningsknapp/meny i e-bokens läsenhet. Du kan också få calibre att automatiskt skapa en *indrags*innehållsförteckning som blir en del av bokens text. Den skapas baserat på den för närvarande definierade innehållsförteckningen.

Om du använder det här verktyget flera gånger kommer varje anrop orsaka den tidigare skapade indragsinnehållsförteckningen att ersättas. Verktyget kan nås via Verktyg → Innehållsförteckning → Infoga en indragsinnehållsförteckning.

Ställa in semantik

Detta verktyg används för att ställa in semantik i EPUB-filer. Semantik är helt enkelt länkar i OPF-filen som identifierar vissa platser i boken som har speciell betydelse. Du kan använda dem för att identifiera förordet, engagemanget, omslaget, innehållsförteckningen osv. Välj helt enkelt vilken typ av semantisk information som du vill ange och välj sedan platsen i boken som länken ska hänvisa till på. Detta verktyg kan nås via Verktyg → Ställ in semantik.

Filtrera formatinformation

Detta verktyg kan användas för att enkelt ta bort specificerade CSS-formatmallar från hela boken. Du kan tala om vilka egenskaper du vill ha bort, till exempel color, background-color, line-height och det kommer att ta bort dem överallt de inträffar — formatmallar, <style> taggar och indragsformatattribut. Efter borttag av formatformationen, en sammanfattning av alla de ändringar som gjorts visas så att du kan se exakt vad som ändrats. Verktyget kan nås via Verktyg → Filtrera formatinformation.

Uppgraderar bokens inre delar

Detta verktyg kan användas för att uppgradera bokens inre delar, om möjligt. Till exempel kommer det att uppgradera EPUB 2 böcker till EPUB 3 böcker. Verktyget kan nås via Uppgradera bokens inre delar.

Kontrollpunkter

Kontrollpunkter är ett sätt att markera det aktuella tillståndet i boken som ”speciellt”. Du kan sedan gå vidare till att göra de ändringar du vill på boken och om du inte gillar resultatet, återgå till kontrollpunktstillståndet. Kontrollpunkter skapas automatiskt varje gång du kör någon av de automatiserade verktyg som beskrivs i föregående avsnitt.

Du kan skapa en kontrollpunkt via Redigera → Skapa kontrollpunkt. Och gå tillbaka till en tidigare kontrollpunkt med Redigera → Återställ till …

Kontrollpunktsfunktionen är utöver den normala ångra/gör om-mekanismen vid redigering av enskilda filer. Kontrollpunkter behövs när ändringar är spridda över flera filer i boken eller när du vill kunna återställa en stor grupp relaterade ändringar som helhet.

Du kan se en lista med tillgängliga kontrollpunkter via Visa → Kontrollpunkter. Du kan jämföra det aktuella tillståndet i boken till en angiven kontrollpunkt med verktyget Jämföra e-böcker – genom att välja kontrollpunkt av intresse och klicka på knappen Jämför. Knappen Återgå till återställer boken till den valda kontrollpunkten, ångra alla ändringar efter den kontrollpunkten skapades.

Panelen för realtidsförhandsvisning

Förhandsvisa fil ger dig en direkt överblick över de olika filerna inuti realtidsförhandsvisningspanelen som visar dig ändringarna du gör i realtid (med försening av en eller två sekunder). När du redigerar HTML- eller CSS-filer uppdateras förhandsvisningspanelen automatiskt för att återspegla dina ändringar. När du flyttar runt markören i redigeraren kommer förhandsvisningspanelen spåra dess plats och visar motsvarande plats i boken. Genom att klicka i förhandsvisningspanelen gör att markören i redigeraren placeras över element du klickade på. Om du klickar på en länk som hänvisar till en annan fil i boken öppnas den filen automatiskt i redigerings- och förhandsvisningspanelen.

Du kan stänga av den automatiska synkroniseringen av position och realtidsförhandsvisning av ändringar – med knapparna under förhandsvisningspanelen. Realtidsuppdateringen av förhandsvisningspanelen sker bara när du inte aktivt skriver i redigeraren, för att inte distrahera eller sakta ner dig i väntan på att förhandsvisningen ska återges.

Förhandsvisningspanelen visar hur texten kommer att se ut när de visas. Dock är förhandsvisningspanelen inte en ersättning för att faktiskt testa din bok på en verklig läsenhet. Det är både mer och mindre kapabelt än en verklig läsenhet. Det tolererar fel och slarvig markering mycket bättre än de flesta läsenheter. Det visar inte heller sidmarginaler, sidbrytningar och inbäddade teckensnitt som använder alias för teckensnittsnamn. Använd förhandsvisningspanelen medan du arbetar med boken, men när du är klar, granska den i en verklig läsenhet eller programvaruemulator.

Observera

Förhandsvisningspanelen stöder inte inbäddade teckensnitt om namnet på teckensnittet inne i teckensnittsfilen inte matchar namnet i ”CSS @font-face”-regeln. Du kan använda verktyget Kontrollera bok för att snabbt hitta och åtgärda eventuella problemteckensnitt.

Dela upp HTML-filer

Ett, kanske icke självklar, användning av förhandsvisningspanelen är att dela upp långa HTML-filer. Medan du tittar på filen som du vill dela klickar du på knappen Delat läge under förhandsvisningspanelen . Flytta sedan helt enkelt din mus till den plats där du vill dela filen och klicka. En tjock grön linje kommer att visa dig exakt var delningen kommer att ske när du flyttar din mus. När du har hittat den plats du önskar klickar du helt enkelt och delning kommer att utföras.

Om du delar upp filen uppdateras automatiskt alla länkar och referenser som hänvisade in i den nedre halvan av filen och öppnar den nyligen delade filen i en redigerare.

Du kan också dela upp en enda HTML-fil på flera platser automatiskt, genom att högerklicka inne i filen i redigeraren och välja Dela upp på flera ställen. Detta gör att du enkelt delar upp en stor fil på alla rubriktaggar eller alla taggar som har en viss klass och så vidare.

CSS-realtidspanelen

CSS-realtids-panelen visar alla formatregler som gäller för taggen du för närvarande redigerar. Namnet på taggen, tillsammans med dess radnummer i redigeraren visas, följt av en lista med matchande formatregler.

Det är ett bra sätt att snabbt se vilka formatregler som gäller för en tagg. Vyn har också klickbara länkar (i blått), som tar dig direkt till platsen där formatet definierades, om du vill göra några ändringar i formatregler. Formatregler som gäller direkt för taggen, samt regler som ärvs från överordnade taggar visas.

Panelen visar även hur de slutligen beräknade format för taggen är. Egenskaper i listan som ersätts av högre prioriteringsregler visas med en linje genom dem.

Du kan aktivera CSS-realtidspanelen via Visa → Direkt CSS.

Diverse verktyg

Det finns några fler verktyg som kan vara användbara när du redigerar boken.

Innehållsförteckningsvyn

Innehållsförteckningsvyn visar aktuell innehållsförteckning i boken. Genom att dubbelklicka på en post öppnas den plats som posten hänvisar till i en redigerare. Du kan högerklicka för att redigera innehållsförteckningen, uppdatera vyn eller fäll ut/fäll in alla poster. Åtkomst till denna vy via Visa → Innehållsförteckning.

Kontrollera stavning av ord i boken

Du kan köra en stavningskontroll via Verktyg → Kontrollera stavning.

Ord visas med antalet gånger de förekommer i boken och språket ordet tillhör. Språkinformation tas från böckernas metadata och från lang-attributen i HTML-filerna. Detta gör att stavningskontrollen fungerar bra även med böcker som innehåller text på flera språk. Till exempel i följande HTML-utdrag kommer ordfärgen att kontrolleras med hjälp av amerikansk engelska och ordfärgen med brittisk engelska:

<div lang="en_US">color <span lang="en_GB">colour</span></div>

Observera

Du kan dubbelklicka på ett ord för att markera nästa förekomst av det ordet i redigeraren. Detta är användbart om du vill manuellt redigera ordet eller se vilket sammanhang det är i.

För att ändra ett ord, helt enkelt dubbelklicka en av de föreslagna alternativa stavningar på höger eller skriv in ditt eget korrigerade stavning och klicka på knappen Ändra valda ord till. Detta kommer att ersätta alla förekomster av ordet i boken. Du kan också högerklicka på ett ord i huvud ordlistan ändra ordet lämpligen från högerklicksmenyn.

Du kan låta stavningskontrollen ignorerar ett ord för den aktuella sessionen genom att klicka på knappen Ignorera. Du kan även lägga till ett ord i ordboken genom att klicka på Lägg till i ordbok. Stavningskontrollen har stöd för flera ordböcker, så att du kan välja den ordbok du vill att ordet läggs till.

Du kan också låta stavningskontrollen visa alla ord i din bok, inte bara de felaktigt stavade. Detta är användbart för att se vilka ord som är vanligast i din bok och för att göra en enkel sökning och ersätta enskilda ord.

Observera

Om du gör några ändringar i boken genom att redigera filer medan verktyget stavningskontroll är öppet, ska du klicka på knappen Uppdatera i verktyget Stavningskontroll. Om du inte gör detta och fortsätter att använda verktyget Stavningskontroll, kan du förlora de ändringar du har gjort i redigeraren.

Observera

För att exkludera en enskild fil från att stavningskontrolleras när du kör stavningskontrollverktyget, kan du använda knappen Exkludera filer eller lägga till följande kommentar precis under öppningstaggen i filen:

<!-- calibre-no-spell-check -->

Lägga till nya ordböcker

Stavningskontrollen har inbyggda ordböcker för engelska och spanska. Du kan installera dina egna ordböcker via Redigera → Inställningar → Redigerarinställningar → Hantera stavningsordböcker. Stavningskontrollen kan använda ordböcker från LibreOffice-programmet (i .oxt-format). Du kan hämta dessa ordböcker från LibreOffice-tilläggsförrådet [https://extensions.libreoffice.org/?Tags%5B%5D=50].

Infoga specialtecken

Du kan infoga tecken som är svåra att skriva med hjälp av verktyget Redigera → Infoga specialtecken. Detta visar alla Unicode-tecken, klicka på det tecken du vill skriva. Om du håller Ctrl medan du klickar stängs fönstret efter att det valda tecknet har infogats. Det här verktyget kan användas för att infoga specialtecken i huvudtexten eller i något annat område i användargränssnittet, till exempel verktyget Sök och ersätt.

Eftersom det finns en hel del tecken kan du definiera dina egena Favorit karaktärer, som kommer att visas först. Högerklicka på ett tecken för att markera den som favorit. Du kan också högerklicka på en karaktär i favoriter för att ta bort den från favoriter. Slutligen kan du ordna om ordningen på tecknen i favoriter genom att klicka på Ordna om favoriter knappen och sedan dra och släppa tecknen i favoriter runt omkring.

Du kan också skriva in specialtecken direkt med tangentbordet. För att göra detta skriver du Unicode-koden för tecknet (i hexadecimal) och trycker sedan på Alt+X som kommer att konvertera den tidigare skrivna koden till motsvarande tecken. Om du till exempel skriver ÿ skriver du ff och sedan Alt+X. För att skriva ett icke-brytande blanksteg skulle du använda a0 och sedan Alt+X, för att skriva den horisontella ellipsen skulle du använda 2026 och Alt+X och så vidare.

Slutligen kan du skriva in specialtecken med hjälp av HTML-enheter. Till exempel att skriva &nbsp; kommer att ersättas av ett hårt blanksteg när du skriver semikolon. Bytet sker bara när du skriver semikolon.

Kodinspektörsvyn

Denna vy visar HTML-kodning och CSS som gäller för det aktuella elementet av intresse. Du öppnar den genom att högerklicka på en plats i förhandsvisningspanelen och välja Granska. Det gör att du kan se HTML-kodning för det elementet och ännu viktigare, de CSS-format som gäller för den. Du kan till och med dynamiskt redigera formaten och se vilken effekt dina ändringar har direkt. Observera att redigera formaten faktiskt inte gör ändringar i bokens innehåll, det innebär bara ett snabb experimenterande. Förmågan att redigera direkt inne i inspektören är under utveckling.

Kontrollera externa länkar

Du kan använda detta verktyg för att kontrollera alla länkar i din bok som hänvisar till externa webbplatser. Verktyget kommer att försöka besöka varje externt länkad webbplats, och om besöket misslyckas, kommer det att rapportera alla trasiga länkar i ett behändigt format för dig att åtgärda.

Hämta externa resurser

Du kan använda detta verktyg för att automatiskt hämtar några bilder/formatmallar/osv. i boken som inte är kombinerade med boken (dvs. de har URL:er som hänvisar till en plats på internet). Verktyget kommer att hitta alla dessa resurser och automatiskt hämtar dem, lägga till dem i boken och ersätta alla hänvisningar till dem att använda de hämtade filerna.

Ordna filer i mappar efter typ

Ofta när du redigerar EPUB-filer som du får någonstans ifrån, kommer du att upptäcka att filerna inuti EPUB:en är ordnade på måfå, i olika undermappar. Det här verktyget gör det möjligt att automatiskt flytta alla filer i undermappar baserat på deras typer. Få åtkomst till det via Verktyg → Ordna i mappar. Observera att det här verktyget bara ändrar hur filerna är ordnade inne i EPUB:en, det ändrar inte hur de visas i filhanteraren.

Importera filer i andra e-bokformat som EPUB

Redigeraren inkluderar möjligheten att importera filer i vissa andra e-bokformat direkt som en ny EPUB, utan att gå igenom en full konvertering. Detta är särskilt användbart för att direkt skapa EPUB-filer från dina egna handredigerade HTML-filer. Du kan göra detta via Arkiv → Importera en HTML- eller DOCX-fil som en ny bok.

Rapportverktyget

Redigeraren har ett trevligt Rapport verktyg (via Verktyg → Rapporter) som visar sammanfattningar av filer, bilder, länkar, ord, tecken och format som används i boken. Varje rad i rapporten är hyperlänkad. Dubbelklicka på en rad hoppar till platsen i boken där posten används eller definieras (i förekommande fall). I vyn Länkar kan du till exempel dubbelklicka på poster i kolumnen Källa för att hoppa till var länken är definieras och poster i kolumnen Mål för att hoppa till var länken hänvisar.

Speciella funktioner i kodredigeraren
calibre-HTML-redigeraren är mycket kraftfull. Den har många funktioner som gör redigering av HTML (och CSS) enklare.

Syntaxmarkering

HTML-redigeraren har mycket sofistikerad syntaxmarkering. Funktioner inkluderar:

- Texten inne i fet, kursiv och rubriktaggar görs fet/kursiv

- När du flyttar markören genom HTML-koden markeras de matchande HTML-taggarna och du kan hoppa till den inledande eller avslutande taggen med tangentbordsgenvägarna Ctrl+{ och Ctrl+}. På samma sätt kan du välja innehållet i en tagg med Ctrl+Alt+T eller Ctrl+Skift+T.

- Ogiltig HTML markeras med en röd understrykning

- Stavfel i texten inuti HTML-taggar och attribut som titel är markerade. Stavningskontrollen är språkmedveten, baserat på värdet av lang-attributet av den aktuella taggen och det övergripande bokspråket.

- CSS inbäddade inuti <style> taggar är markerad

- Specialtecken som kan vara svåra att skilja såsom icke-brytande blanksteg, olika typer av bindestreck osv. är markerade.

- Länkar till andra filer i <a> taggar, <img>- och <link> taggar har alla filnamnen markerade. Om filnamnet de hänvisar till finns inte, markeras filnamnet med en röd understrykning.

Kontextkänslig hjälp

Du kan högerklicka på ett HTML-taggnamn eller ett CSS-egenskapsnamn för att få hjälp för den taggen eller egenskapen.

Du kan också hålla ned Ctrl-tangenten och klicka på valfritt filnamn i en länktagg för att automatiskt öppna filen i redigeraren. På samma sätt Ctrl genom att klicka på ett klassnamn kommer du till den första formatregeln som matchar taggen och klassen.

Genom att högerklicka på ett klassnamn i en HTML-fil kan du byta namn på klassen, ändra alla förekomster av klassen genom hela boken och alla dess formatmallar.

Automatisk komplettering

När du redigerar en e-bok är en av de tråkigaste uppgifterna att skapa länkar till andra filer i boken, eller till CSS-formatmallar eller bilder. Du måste räkna ut rätt filnamn och relativ sökväg till filen. Redigeraren kompletterar automatiskt för att göra det lättare.

När du skriver ett filnamn tar redigeraren automatiskt fram förslag. Använd helt enkelt Tab-tangenten för att välja rätt filnamn. Redigeraren ger till och med förslag på länkar som hänvisar till ett ankare inuti en annan HTML-fil. När du har skrivit tecknet # kommer redigeraren att visa dig en lista med alla ankare i målfilen, med ett litet kodavsnitt av text som hjälper dig att välja rätt ankare.

Observera att till skillnad från de flesta andra kompletteringssystem använder redigerarens kompletteringssystem delsekvensmatchning. Detta innebär att du bara kan skriva två eller tre bokstäver från var som helst i filnamnet för att fylla i filnamnet. Säg till exempel att du vill ha filnamnet ../images/arrow1.png, du kan helt enkelt skriva ia1 och trycka Tab för att fylla i filnamn. Vid sökning efter träffar prioriterar kompletteringssystemet bokstäver som finns i början av ett ord, eller omedelbart efter en sökvägsavskiljare. När du väl vant dig vid detta system kommer du att upptäcka att det sparar dig mycket tid och ansträngning.

Kodavsnitt
calibre e-bokredigeraren stöder kodavsnitt. Ett kodavsnitt är ett stycke text som antingen återanvänds ofta eller innehåller mycket överflödig text. Redigeraren kan du infoga ett kodavsnitt med endast ett fåtal knapptryckningar. De kodavsnitt är mycket kraftfulla, med många funktioner, som platshållare som du kan hoppa mellan, automatisk spegling av upprepad text och så vidare. För mer information se Kodavsnitt.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Redigera e-böcker »

- Funktionsläge för Sök & ersätt i Redigeraren

Funktionsläge för Sök & ersätt i Redigeraren

Verktyget Sök & ersätt i redigeraren stöder ett funktionsläge. I det här läget kan du kombinera reguljära uttryck (se Allt om att använda reguljära uttryck i calibre) med godtyckligt kraftfulla Python-funktioner för att göra alla typer av avancerad textbehandling.

I standardläget regexp för sök och ersätt anger du både ett reguljärt uttryck för att söka efter och en mall som används för att ersätta alla hittade matchningar. I funktionsläget, istället för att använda en fast mall, anger du en godtycklig funktion, i programmeringsspråket Python [https://docs.python.org]. Det här gör att du kan göra massor av saker som inte är möjliga med enkla mallar.

Tekniker för att använda funktionsläge och syntaxen beskrivs med hjälp av exempel, som visar dig hur du skapar funktioner för att utföra allt mer komplexa uppgifter.

Automatiskt justera skiftläget med rubrikerna i dokumentet

Här kommer vi att utnyttja en av de inbyggda funktioner i redigeraren för att automatiskt ändra skiftläget för all text i rubriktaggar till versal:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

För funktionen, helt enkelt välj inbyggd funktion Titelskiftlägestext. Det kommer att ändra titlar som ser ut: <h1>någon TITEL</h1> till <h1>Någon Titel</h1>. Det kommer även att fungera om det finns andra HTML-taggar inuti rubriktaggar.

Din första anpassade funktion - smartare bindestreck

Den verkliga kraften i funktionsläge kommer från att kunna skapa dina egna funktioner för att bearbeta text på godtyckliga sätt. Förbättra skiljeteckenhanteringsverktyget i redigeraren lämnar enskilda bindestreck i fred, så att du kan använda denna funktion för att ersätta dem med em-streck.

För att skapa en ny funktion, klickar du helt enkelt på knappen Skapa/redigera för att skapa en ny funktion och kopiera Python-koden från nedan.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

Varje Sök och ersätt anpassad funktion måste ha ett unikt namn och bestå av en Python-funktion med namnet replace, som accepterar alla argument som visas ovan. För tillfället kommer vi inte oroa oss för alla olika argument för funktionen replace(). Fokusera bara på argumentet match. Det representerar en matchning när du söker och ersätter. Dess fullständiga dokumentation finns tillgänglig här [https://docs.python.org/library/re.html#match-objects]. match.group() returnerar helt enkelt all den matchad text och allt vi gör är att ersätta bindestreck i den texten med em-bindestreck, ersätt först dubbla bindestreck och sedan enkla bindestreck.

Använd den här funktionen med det reguljära uttrycket hitta:

>[^<>]+<

Och det kommer att ersätta alla bindestreck med em-streck, men bara i själva texten och inte inne i HTML-taggdefinitioner.

Kraften av funktionsläge - med hjälp av en stavningsordboken att åtgärda felaktigt avstavade ord

Ofta innehåller böcker skapade av skanningar av tryckta böcker felavstavade ord – ord som delades vid slutet av raden på den utskrivna sidan. Vi kommer att skriva en enkel funktion för att automatiskt hitta och åtgärda sådana ord.

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

Använd denna funktion med samma sökuttryck som tidigare, det vill säga:

>[^<>]+<

Och det kommer magiskt åtgärda alla felavstavade ord i texten i boken. Huvudtricket är att använda en av de användbara extra argument till replace-funktionen dictionaries. Detta syftar på de ordböcker redigeraren själv använder för att stavningskontrollera text i boken. Vad denna funktion gör är att leta efter ord separerade med ett bindestreck, tar bort bindestrecket och kontrollerar om ordboken godkänner sammansatt ordet, om den gör det, ersättas de ursprungliga orden med de bindestrecksfria sammansatta orden.

Observera att en begränsning av denna teknik är att den bara fungerar för enspråkiga böcker, eftersom dictionaries.recognized() som standard använder bokens huvudspråk.

Automatisk numrera avsnitt

Nu kommer vi att se något lite annorlunda. Anta att din HTML-fil har många avsnitt, var och en med en rubrik i en <h2> tagg som ser ut som <h2>Någon text</h2>. Du kan skapa en anpassad funktion som automatiskt numrerar dessa rubriker med avsnittsnummer i följd, så att de ser ut <h2>1. Någon text</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'

Använd det med sökuttrycket:

(?s)(<h2[^<>]*>)(.+?</h2>)

Placera markören överst i filen och klicka på Ersätt alla.

Den här funktionen använder ett annat av de användbara extraargumenten för argumentet replace(): number. När du gör ett Ersätt alla-räknas numret upp automatiskt för varje efterföljande träff.

En annan ny funktion är användningen av replace.file_order – inställningen att 'spine' betyder att om denna sökning körs på flera HTML-filer bearbetas filerna i den ordning de visas i boken. Se Välj filordning när flera HTML-filer körs för detaljer.

Skapa automatiskt en innehållsförteckning

Slutligen, låt oss testa något lite mer ambitiöst. Antag att din bok har rubriker i h1 och h2 taggar som ser ut som <h1 id="someid">Någon text</h1>. Vi kommer automatiskt skapa en HTML-innehållsförteckning baserad på dessa rubriker. Skapa den anpassade funktionen nedan:

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'

Och använd den med sökuttrycket:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

Kör sökningen på Alla textfiler och vid slutet av sökningen, kommer ett fönster dyka upp med ”Felsök utmatning från din funktion” vilken kommer ha HTML-innehållsförteckning, klar att klistras in i toc.html.

Funktionen ovan är kraftigt kommenterad, så den borde vara lätt att följa. Den viktigaste nya funktionen är användningen av ett annan användbart extra argument till funktionen replace(), objektet data. Objektet data är en Python ordbok som kvarstår mellan alla efterföljande anrop av replace() under en enda Ersätt alla operation.

En annan ny funktion är användningen av call_after_last_match – genom att ställa in det till True på funktionen replace() innebär det att redigeraren kommer att anropa replace() en extra gång efter att alla träffar har hittats. För detta extra anrop kommer träffobjektet vara None.

Det var bara en demonstration för att visa styrkan av funktionsläge, om du verkligen behövde skapa en innehållsförteckning från rubriker i din bok, kommer du ha större nytta av att använda det dedikerade innehållsförteckningsverktyget i Vertyg → Innehållsförteckning.

API för funktionsläget

Alla funktionsläge funktioner måste vara Python-funktioner som heter ”replace” med följande signatur:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return a_string

När en sök/ersätt körs, för varje träff som hittas kommer funktionen replace() att anropas, den måste returnera ersättningssträngen för den träffen. Om inga ersättare ska göras, bör den returnera match.group() som är den ursprungliga strängen. De olika argumenten för funktionen replace() dokumenteras nedan.

Argumentet match

Argumentet match representerar den aktuella hittade matchningen. Det är ett Python objekt [https://docs.python.org/library/re.html#match-objects]. Den användbaraste metoden är group() som kan användas för att få den matchande texten som motsvarar enskilda fångstgrupper i reguljära sökuttrycket.

Argumentet number

Argumentet nummer är numret på den aktuella träffen. När du kör Ersätt alla, kommer varje efterföljande träff att orsaka replace() att anropas med ett ökande antal. Första träff har nummer 1.

Argumentet file_name

Det här är filnamnet på filen däre den aktuella träffen hittades. När du söker inuti markerad text är file_name tom. file_name är i kanonisk form, en relativ sökväg till roten av boken, med / som sökvägsavgränsare.

Argumentet metadata

Detta representerar metadata för den aktuella boken, som titel, författare, språk o.s.v. Det är ett objekt av klassen calibre.ebooks.metadata.book.base.Metadata. Användbara egenskaper inkluderar, title, authors (en lista med författare) och language (språkkod).

Argumentet dictionaries

Detta innebär insamling av ordböcker som används för stavningskontroll den aktuella boken. Den användbaraste metoden dictionaries.recognized(word) vilken som returnerar True om givet in ord känns igen av ordbok för den aktuella bokens språk.

data-argumentet

Detta är en enkel Python ordbok. När du kör Ersätt alla kommer varje efterföljande träff att orsaka replace() att anropas med samma ordbok som data. Du kan alltså använda den för att lagra godtyckliga data mellan anrop av replace() under en Ersätt alla-operation.

functions-argumentet
functions-argumentet ger dig tillgång till alla andra användardefinierade funktioner. Detta är användbart för kodåteranvändning. Du kan definiera nyttofunktioner på ett ställe och återanvända dem i alla dina andra funktioner. Anta till exempel att du skapar ett funktionsnamn My Function så här:

def utility():
# do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

Sedan, i en annan funktion kan du komma åt utility() funktionen så här:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
utility = functions['My Function']['utility']
...

Du kan också använda funktionsobjektet för att lagra beständiga data, som kan återanvändas av andra funktioner. Till exempel kan du ha en funktion som när den körs med Ersätt alla samlar in en del data och en annan funktion som använder den när den körs efteråt. Betrakta följande två funktioner:

# 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']
...

Felsökning av dina funktioner

Du kan felsöka de funktioner som du skapar med hjälp av standard print() funktionen från Python. Utskriften av print kommer visas i ett extrafönster efter att Sök/ersätt är klar. Du såg ett exempel på att använda print() för att mata ut en hel innehållsförteckning ovan.

Välj filordning när flera HTML-filer körs

När du kör Ersätt alla på flera HTML-filer beror ordningen som filerna behandlas på på vilka filer du har öppna för redigering. Du kan tvinga sökningen att behandla filer i den ordning de visas genom att ställa in attributet file_order i din funktion, så här:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.file_order = 'spine'

file_order accepterar två värden, spine och spine-reverse vilka får sökprocessen att behandla flera filer i den ordning de dyker upp i boken, antingen framåt eller bakåt.

Att få din funktion anropad en extra gång efter att den senaste träffen hittats

Ibland, som i exemplet med den automatiskt skapande av innehållsförteckning ovan, är det användbart att få din funktion anropad en extra gång efter att den sista träffen hittats. Du kan göra detta genom att ställa in attributet call_after_last_match på din funktion, så här:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.call_after_last_match = True

Lägga till utmatning från funktionen till markerad text

När du kör sök och ersätt på markerad text är det ibland användbart att lägga till sådan text i slutet av den markerade texten. Du kan göra det genom att ställa in attributet append_final_output_to_marked på din funktion (observera att du också måste ställa in call_after_last_match), så här:

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

Undertryck resultatdialogrutan när sökning sker med markerad text

Du kan också undertrycka resultatdialogrutan (som kan bromsa upp upprepad tillämpning av en sök/ersätt på många textblock ) genom att ställa in suppress_result_dialog attributet på din funktion, så här:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.suppress_result_dialog = True

Fler exempel

Mer användbara exempel som tillförs av calibre-användare kan hittas i forumet för calibre e-bokredigeraren [https://www.mobileread.com/forums/showthread.php?t=237181].

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Redigera e-böcker »
- Kodavsnitt

Kodavsnitt
calibre e-bokredigeraren stöder kodavsnitt. Ett avsnitt är ett stycke text som antingen återanvänds ofta eller innehåller mycket överflödig text. Redigeraren kan du infoga ett kodavsnitt med endast ett fåtal knapptryckningar. Anta till exempel att du ofta infogar länktaggar när du redigerar HTML-filer, då kan du helt enkelt skriva <a i redigeraren och trycka Control+J. Redigeraren kommer att utöka det till:

<a href="filename"></a>

Inte bara det, ordet filnamn kommer att väljas, med markören placeras över den, så att du enkelt kan skriva in det verkliga filnamnet med med redigerarens smidiga Automatisk komplettering-funktion. Och när du är klar med att skriva filnamnet, tryck på Control+J igen och markören hoppar till positionen mellan taggarna <a> så att du enkelt kan skriva in texten för länken.

Kodavsnittssystemet i redigeraren är mycket sofistikerad, det finns några inbyggda kodavsnitt och du kan skapa din egen som passar din redigeringsstil.

Följande diskussion av de inbyggda kodavsnitt bör hjälpa illustrera kraften i kodavsnittssystemet.

Observera

Du kan också använda kodavsnitt i textinmatningsfält i Sök och ersätt-panelen, dock platshållare (med Control+J för att hoppa runt) kommer inte att fungera.

Inbyggda kodavsnitt

De inbyggda kodavsnitten beskrivs nedan. Observera att du kan åsidosätta dem genom att skapa dina egna kodavsnitt med samma utlösartext.

Infoga fyllnadstext [Lorem]

Det första inbyggda kodavsnitt, och det enklaste används för att infoga fyllnadstext i ett dokument. Fyllnadstexten är hämtad från De finibus bonorum et malorum [https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum] ett filosofiskt verk av Cicero (översatt till engelska). För att använda den, skriv helt enkelt Lorem i en HTML-fil och tryck Control+J. Det kommer att ersättas av ett par stycken av fyllnadstext.

Definitionen av detta kodavsnitt är mycket enkel, utlösartexten definieras som Lorem och mallen definieras helt enkelt som den bokstav texten som ska infogas. Du kan enkelt anpassa den för att använda din favorit form av fyllnadstext.

Infoga en självstängande HTML-tagg [<>]

Nu ska vi titta på ett enkelt exempel på den kraftfulla begreppet platshållare. Säg att du vill infoga självstängande taggen <hr/>. Skriv bara <> och tryck Control+J så kommer redigeraren utvidga kodavsnittet till:

<|/>

Här representerar | symbolen den aktuella markörpositionen. Du kan sedan skriva hr och trycka Control+J för att flytta markören till efter slutet av taggen. Detta kodavsnitt definieras som:

Trigger: <>
Template: <$1/>$2

Platshållare är helt enkelt dollar ($)-tecken följt av ett nummer. När kodavsnittet utvidgas genom att trycka Control+J är markören placerad vid den första platshållaren (platshållaren med det lägsta numret). När du trycker på Control+J igen markören hoppar till nästa platshållare (platshållaren med nästa högre nummer).

Infoga en HTML-länktagg [<a]

HTML-länktaggar delar alla en gemensam struktur. De har ett href attribut och lite text mellan start- och sluttaggen. Ett kodavsnitt att skriva in dem mer effektiva kommer att introducera oss till några fler funktioner i platshållare. För att använda detta kodavsnitt, skriv helt enkelt <a och tryck Control+J. Redigeraren kommer att utvidga detta till:

<a href="filename|"></a>

Inte bara det, ordet filnamn kommer att väljas, med markören placeras över den, så att du enkelt kan skriva in det verkliga filnamnet med med redigerarens smidiga Automatisk komplettering-funktion. Och när du är klar med att skriva filnamnet, tryck på Control+J igen och markören hoppar till positionen mellan taggarna <a> så att du enkelt kan skriva in texten för länken. När du är klar med att skriva texten trycker du på Control+J igen för att hoppa till punkten efter den avslutande taggen. Detta kodavsnitt definieras som:

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

Det finns ett par nya funktioner här. Först $1 platshållare har blivit mer komplex. Den innehåller nu en del standardtext (ordet `` filename``). Om en platshållare innehåller standardtext, är standardtexten istället för platshållaren när kodavsnittet utvidgas. Även när du hoppar till en platshållare med standardtext som använder Control+J, väljs standardtexten. På detta sätt kan du använda standardtexten som en påminnelse till dig att fylla i viktiga delar av mallen. Du kan ange standardtext för en platshållare genom att använda syntaxen: ${<number>:default text}.

Den andra nya funktionen är att den andra platshållaren har en asterisk efter sig (${2*}). Detta innebär att all text som valdes innan mallen utvidgades ersätter platshållaren. För att se detta i handling, välja någon text i redigeraren, tryck Control+J, typ <a och tryck Control+J igen, mallen kommer att utökas till:

<a href="filename">whatever text you selected</a>

Infoga en HTML-bildtagg [<i]

Detta är mycket likt att infoga en HTML-länk, som vi såg ovan. Det gör att du snabbt kan mata in en <img src="filename" alt="description" /> tagg och hoppa mellan src och alt attribut:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

Infoga en godtycklig HTML-tagg [<<]

Detta låter dig infoga en godtycklig fullständig HTML-tagg (eller radbryta tidigare markerad text i taggen). För att använda det, skriv helt enkelt << och tryck Control+J. Redigeraren kommer att utöka den till:

<|></>

Skriv taggnamnet, till exempel: span och tryck Control+J, som kommer att resultera i:

<span>|</span>

Du kommer att observera att den avslutande taggen automatiskt har fyllts med span. Detta uppnås med ännu ett annat särdrag av platshållare, spegling. Spegling betyder helt enkelt att om du anger provplatshållaren mer än en gång i en mall, den andra och alla senare positioner kommer automatiskt fyllas i med vad du skriver i det första positionen, när du trycker Control+J. Definitionen för detta kodavsnitt är:

Trigger: <<
Template: <$1>${2*}</$1>$3

Som ni kan se, har den första platshållaren ($1) specificerats två gånger, andra gången i den avslutande taggen, som helt enkelt kommer att kopiera vad du skriver i öppningstaggen.

Infoga en godtycklig HTML-tagg med ett klassattribut [<c]

Detta är mycket likt infogningen av godtycklig tagg i exemplet ovan, förutom att det förutsätter att du vill ange en klass för taggen:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

Detta gör att du kan först skriva taggnamnet, tryck Control+J, skriv klassnamnet, tryck Control+J, skriv innehållet i taggen och tryck Control+J en sista gång för att hoppa ur taggen. Den avslutande taggen kommer att automatiskt fyllas.

Skapa dina egna kodavsnitt

Kodavsnitt lyser verkligen eftersom du kan skapa dina egna för att passa din redigeringsstil. För att skapa egna kodavsnitt gå till Redigera → Inställningar → Redigerarinställningar → Hantera kodavsnitt i redigeraren. Detta kommer att ta fram en lättanvänd dialogruta för att hjälpa dig att skapa dina egna kodavsnitt. Klicka helt enkelt på knappen Lägg till kodavsnitt och du kommer att se en dialogruta som ser ut:

Först ge ditt kodavsnitt ett namn, något beskrivande, för att hjälpa till att identifiera texturvalet i framtiden. Ange sedan utlösaren. En utlösare är helt enkelt den text som du måste skriva i redigeraren innan du trycker Control+J för att utöka kodavsnittet.

Ange sedan kodavsnittsmallen. Du bör börja med ett av exemplen ovan och ändra det för att passa dina behov. Slutligen ange vilka filtyper du vill att kodavsnittet ska vara aktivt för. På så sätt kan du ha flera kodavsnitt med samma utlösartext som fungerar annorlunda i olika filtyper.

Nästa steg är att testa ditt nyskapade kodavsnitt. Använd tutan Test längst ner. Skriv in utlösartexten och tryck Control+J för att utvidga kodavsnittet och hoppa mellan platshållare.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- calibre-innehållsservern

calibre-innehållsservern
calibre-innehållsservern gör att du kan komma åt dina calibre bibliotek och läsa böcker direkt i en webbläsare på din favorit mobiltelefon eller surfplatta. Som ett resultat behöver du inte installera några dedikerade bokläsnings-/hanteringsappar på din telefon. Använd bara webbläsaren. Servern hämtar och lagrar boken du läser i ett frånkopplat cache så att du kan läsa den även när det inte finns någon internetuppkoppling.

Innehåll

- Åtkomst till innehållsservern från andra enheter

- Åtkomst till servern från enheter i ditt hemnätverk

- Åtkomst till servern från var som helst på internet

- Servergränssnittet

- Boklistan

- Bokläsaren

- Webbläsarstöd

- Aktivera frånkopplat stöd

- Hantera användarkonton endast från kommandoraden

- Integrera calibre-innehållsservern med andra servrar

- Använda en fullständig virtuell värd

- Använda ett URL-prefix

- Skapa en tjänst för calibre-servern på ett modernt Linux-system

För att starta servern klickar du på knappen Anslut/dela och välj Starta innehållsserver. Det kan hända att du får ett meddelande från datorns brandvägg eller antivirusprogram om det är OK att tillåta tillgång till calibre.exe. Klicka på knappen Tillåt eller OK. Öppna sedan en webbläsare (helst Chrome eller Firefox) i din dator och skriv in följande adress:

http://127.0.0.1:8080

This will open a page in the browser showing you your calibre libraries, click on any one and browse the books in it. Click on a book, and it will show you all the metadata about the book, along with buttons to Read and Download the book. Click the Read button to start reading the book.

Observera

Adressen som används ovan http://127.0.0.1: 8080 fungerar bara på datorn som kör calibre. För att komma åt servern från andra datorer/telefoner/surfplattor/osv. kommer du att behöva göra lite mer arbete, som beskrivs i nästa avsnitt.

Åtkomst till innehållsservern från andra enheter

Det finns två typer av fjärråtkomst som du normalt behöver. Den första, enklare sorten är från ditt hemnätverk. Om du kör calibre på en dator i ditt hemnätverk och du också har anslutit dina andra enheter till samma hemnätverk, borde du enkelt kunna komma åt servern på de här enheterna.

Åtkomst till servern från enheter i ditt hemnätverk

När du har startat servern i calibre som beskrivits ovan klickar du på knappen Anslut/dela igen. Istället för åtgärden guilabel:Starta innehållserver, bör du se en Stoppa Innehållsservern-åtgärd istället. Till höger om denna åtgärd visas en IP-adress och portnummer. Dessa ser ut som en massa siffror åtskilda av perioder. Till exempel:

Stop Content server [192.168.1.5, port 8080]

Dessa siffror berättar vilken adress som ska användas för att ansluta till servern på dina enheter. Efter exemplet ovan blir adressen:

http://192.168.1.5:8080

Den första delen av adressen är alltid http://, nästa delen är IP-adressen, vilken är siffrorna före kommatecken och till sist har vi portnumret som måste läggas till IP-adressen med ett kolon (:). Om du har tur ska det vara allt du behöver och du kommer att se calibre-biblioteken på din enhet. Om inte, läs vidare.

Problemlösning av hemnätverksanslutningen

Om du inte kan komma åt servern från din enhet kan du försöka med följande steg:

1. Kontrollera att servern körs genom att öppna adressen http://127.0.0.1:8080 i en webbläsare som körs på samma dator som servern.

2. Kontrollera att din brandvägg/antivirusprogram tillåter anslutningar till din dator på porten 8080 och till calibre-programmet. Det enklaste sättet att eliminera brandväggen/antivirusprogrammet som källan till problem är att tillfälligt stänga av dem och sedan försöka ansluta. Du bör först koppla från internet, innan du slår av brandväggen, för att hålla datorn säker.

3. Kontrollera att din enhet och dator finns på samma nätverk. Det innebär att de båda ska vara anslutna till samma trådlösa router. I synnerhet bör ingendera använda en mobil- eller ISP tillhandahållen direkt-WiFi-anslutning.

4. Om du har en icke-standardiserad nätverksinstallation kan det vara att IP-adressen som visas på Anslut/dela-menyn är felaktig. I så fall måste du ta reda på vad den rätta IP-adressen du ska använda är själv. Tyvärr, med tanke på oändliga mångfald av nätverkskonfigurationer är det inte möjligt att ge dig en anvisningar för att göra det.

5. Om du har anpassat ett användarnamn och lösenord, försök först utan det för att se om det orsakar problem. Vissa e-ink-enheter har webbläsare som inte hanterar autentisering. Ibland kan du lösa det här genom att inkludera användarnamnet och lösenordet i URL:en, till exempel: http://användarnamn:lösenord@192.168.1.2:8080.

6. Om du har fastnat kan du alltid be om hjälp i calibre-användarforum [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Åtkomst till servern från var som helst på internet

Varning

Innan du gör det bör du aktivera användarnamn/lösenords-skydd i servern, annars kan vem som helst i världen få tillgång till dina böcker. Gå till Inställningar → Delning → Dela via nätet och aktivera alternativet Kräv användarnamn och lösenord för att komma åt innehållsservern.

Medan de specifika detaljerna om att ställa in Internet-åtkomst varierar beroende på nätverkskonfigurationen och typen av dator du använder, är grundschemat det som följer.

1. Ta reda på den externa IP-adressen för den dator du ska köra servern på. Du kan göra det genom att besöka webbplatsen What is my IP address [https://www.whatismyip.com/] med en webbläsare som körs på datorn.

2. Om datorn ligger bakom en router, aktivera portvidarebefordring på routern för att vidarebefordra porten 8080 (eller den port du väljer att köra calibre-innehållsservern på) till datorn.

3. Kontrollera att calibre-servern är tillåten genom alla brandväggar/antivirusprogram på din dator.

4. Nu bör du kunna komma åt servern på vilken internetansluten enhet som helst med hjälp av den IP-adress du hittade i det första steget. Till exempel om den IP-adressen du hittade var 123.123.123.123 och porten du använder för calibre-servern är 8080, blir adressen som används på din enhet: http://123.123.123.123:8080.

5. Alternativt kan du använda en tjänst som no-ip [https://www.noip.com/free] för att ställa in en adress som är lätt att komma ihåg att använda istället för den IP-adress som du hittade i det första steget.

Observera

För högsta säkerhet bör du också aktivera HTTPS på innehållsservern. Du kan antingen göra det direkt på servern genom att ange sökvägen till HTTPS-certifikatet som ska användas i de avancerade konfigurationsalternativen för servern, eller så kan du ställa in en omvänd proxy enligt beskrivningen nedan för att använda en befintlig HTTPS-inställning.

Servergränssnittet

Servergränssnittet är en förenklad version av calibres huvudgränssnitt, optimerat för användning med pekskärmar. På startskärmen visas böcker som du håller på att läsa och du kan välja ett calibre-bibliotek att bläddra i. Servern i calibre ger dig tillgång till alla dina bibliotek, inte bara ett enskilt, som tidigare.

Boklistan

Serverns boklista är ett enkelt rutnät av omslag. Tryck på ett omslag för att se detaljerade metadata för en bok eller för att läsa boken. Om du föredrar en mer detaljerad lista kan du ändra standardvyn genom att klicka på de tre vertikala punkterna i övre högra hörnet.

Sortering och sökning i boklistan bör vara en vana för calibre-användare. De kan nås genom att klicka på deras ikoner högst upp till höger. De arbetar båda på exakt samma sätt som i calibre huvudprogrammet. På söksidan kan du även konstruera sökförfrågningar genom att klicka på författare/taggar/osv., precis som du kan med taggbläddraren i huvudprogrammet.

En mycket älskad funktion i huvudprogrammet, virtuella bibliotek finns också i servergränssnittet. Klicka på de tre vertikala prickarna i det övre högra hörnet för att välja ett virtuellt bibliotek.

Bokläsaren

You can read any book in your calibre library by simply tapping on it and then tapping the Read button. The book reader is very simple to operate. You can both tap and swipe to turn pages. Swiping up/down skips between chapters. Tapping the top quarter of the screen gets you the detailed controls and viewer preferences.

Om du lämnar innehållsservern igång kan du till och med öppna samma bok på flera enheter och den kommer ihåg din senaste läspositionen. Om det inte kan du tvinga ett synkronisering genom att trycka i övre toppkvartalet och välja Synkronisera.

Webbläsarstöd

Den nya calibre-servern använder mycket av avancerade HTML 5- och CSS 3-funktioner. Som sådan krävs en modern webbläsare för användning. Den har testats på Android Chrome och iOS Safari samt Chrome och Firefox på skrivbordet.

Servern är försiktig med att använda funktionalitet som antingen har standardiserats eller kommer att standardiseras. Så om den inte fungerar för närvarande med din favorit webbläsare, kommer det förmodligen att göra det så fort webbläsaren har nått ikapp.

Om du använder en särskilt gammal eller begränsad webbläsare eller om du inte gillar att köra JavaScript kan du använda mobil-vyn genom att helt enkelt lägga till /mobile i serveradressen.

Observera

På iOS tillåter Apple endast en enda webbläsarmotor, så Firefox, Chrome och Safari är alla faktiskt samma webbläsare under huven. Det nya servergränssnittet kräver iOS 10.3.2 eller nyare. På Android har servern testats med Chrome-version 58 och nyare.

Aktivera frånkopplat stöd

Browser makers have been trying to force people to use SSL by disabling advanced features in their browsers for plain HTTP connections. One such casualty is ApplicationCache, which was what was used in calibre for offline support. As a result now-a-days sadly, offline mode works only as long as you keep the browser tab open. In addition, in Firefox on Android, you will need to type about:config and create a preference called browser.tabs.useCache and set it to true.

Hantera användarkonton endast från kommandoraden
calibre-programmet har en bra sektion i Inställningar så att du kan hantera användarkonton för servern. Om du vill köra den fristående servern och inte kan köra calibre-huvudprogrammet på samma dator/användarkonto kan du också hantera användare som använder bara kommandoraden.

Du kan hantera användarkonton med flaggan --manage-users till det fristående programmet calibre-server. Antag att du vill lagra användardatabasen i mappen /srv/calibre, då skapar du den genom att köra:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

Följ bara anvisningarna för att skapa användarkonton, ställ in deras behörighet osv. När du är klar kan du köra servern som:

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

Det kommer att använda användarkonton du skapade i föregående steg.

Integrera calibre-innehållsservern med andra servrar

Här kommer vi att visa hur du integrerar calibre-innehållsservern med en annan server. Den vanligaste orsaken för detta är att använda sig av SSL eller att tjäna calibre-biblioteket som en del av en större plats. Den grundläggande tekniken är att köra calibre servern och anpassa en omvänd proxy till den från huvudservern.

En omvänd proxy är när din vanliga server accepterar inkommande förfrågningar och skickar dem till calibre-servern. Den läser sedan svaret från calibre-servern och vidarebefordrar det till klienten. Det betyder att du enkelt kan köra calibre-servern som normalt utan att försöka integrera den nära din huvudserver.

Använda en fullständig virtuell värd

Den enklaste konfigurationen är att ägna en fullständig virtuell värd till calibre-servern. Kör i så fall calibre-servern som:

calibre-server

Ställ nu in den virtuella värden på din huvudserver, till exempel för 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;
}
}

Eller för 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>

Använda ett URL-prefix

Om du inte vill ägna en fullständig virtuell värd till calibre kan du låta den använda ett URL-prefix. Starta calibre-servern som:

calibre-server --url-prefix /calibre --port 8080

Nyckelparametern här är --url-prefix /calibre. Detta gör att innehållsservern kan betjäna alla webbadresser som prefixas av /calibre. För att se detta i verkligheten, besök http://localhost:8080/calibre i din webbläsare. Du borde se den normala innehållsserverwebbplatsen, men nu kommer den att köras under /calibre.

Med Nginx, är konfigurationen som fodras:

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

För Apache, aktivera först proxymodulerna i Apache genom att lägga till följande i httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Den exakta tekniken för att aktivera proxymoduler varierar beroende på din Apache-installation. När du aktiverar proxymodulerna, lägg till följande regler i httpd.conf (eller om du använder virtuella värdar till conf-filen för den virtuella värden i fråga):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

Det är allt, du kommer nu att kunna komma åt calibre-innehållsservern under /calibre-webbadressen i din huvudserver. Ovanstående regler skickar alla förfrågningar under /calibre till calibre-servern som körs på port 8080 och tack vare alternativet --url-prefix ovan hanterar calibre-servern dem transparent.

Observera

När du använder en omvänd proxy ska du berätta att calibre-innehållsservern bara lyssnar på localhost, genom att använda --listen-on 127.0.0.1. På så sätt lyssnar servern bara på anslutningar som kommer från samma dator, dvs. från omvänd proxyn.

Observera

Om du har ställt in SSL för din huvudserver, bör du berätta för calibre-servern att använda grundläggande autentisering istället för digest autentisering, eftersom den är snabbare. För att göra det, skicka alternativet --auth-mode=basic till calibre server.

Skapa en tjänst för calibre-servern på ett modernt Linux-system

Du kan enkelt skapa en tjänst för att köra calibre vid start på ett modernt (systemd [https://www.freedesktop.org/wiki/Software/systemd/]) Linux baserat system. Skapa bara filen /etc/systemd/system/calibre-server.service med innehållet som visas nedan:

[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

Ändra mylinuxuser och mylinuxgroup till vilken användare och grupp du vill att servern ska köras som. Detta bör vara samma användare och grupp som äger filerna i biblioteksmappen för calibre. Observera att det i allmänhet inte är en bra idé att köra servern som root. Ändra också sökvägen till biblioteksmappen för calibre så att den passar ditt system. Du kan lägga till flera bibliotek om det behövs. Se hjälpen för kommandot calibre-server.

Köra nu:

sudo systemctl start calibre-server

för att starta servern. Kontrollera dess status med:

sudo systemctl status calibre-server

För att den ska starta vid uppstart, kör:

sudo systemctl enable calibre-server

Observera
calibre-servern behöver inte en X-server som körs, men den behöver X-biblioteken installerade eftersom vissa komponenter använder dem för att länka mot.

Observera
calibre-servern stöder också systemd-socketaktivering, så du kan också använda det om det behövs.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Jämföra e-böcker

Jämföra e-böcker
calibre innehåller ett integrerat e-bokjämförelseverktyg som kan användas för att se vad som har förändrats inuti en e-bok efter att ha redigerat eller konverterat den. Det kan jämföra böcker i formaten EPUB och AZW3.

För att använda den, antingen öppna e-bok i verktyget för Redigera e-böcker och sedan klicka på Arkiv → Jämför med annan bok eller använd Bokdetaljer-panelen. Om du gör en konvertering från EPUB till EPUB, kommer den ursprungliga EPUB-filen sparas som ORIGINAL_EPUB. Högerklicka på ORIGINAL_EPUB posten i bokdetaljpanelen och välj Jämför med EPUB-format.

Jämförelseverktyg som öppnas kommer att se ut som skärmdumpen nedan. Det visar skillnader i text, färger och bilder hos de valda böckerna.

Förstå jämförelsevyn

Som framgår av skärmdumpen ovan visar jämförelsevyn skillnaderna mellan de två böckerna sida vid sida. Endast skillnaderna, med några rader av sammanhang runt dem visas. Detta gör det enkelt att se i korthet bara vad som ändrades inuti ett stort dokument som en bok.

Tillagd text visas med grön bakgrund, borttagen text med röd bakgrund och ändrad text med blå bakgrund.

Radnummer för all text som förändrats visas på sidorna, vilket gör det lätt att gå till en viss förändring i redigeraren. När du öppnar jämförelseverktyget inifrån redigeraren kan du också dubbelklicka på en rad i den högra panelen för att automatiskt gå till den raden i redigeraren.

En användbar teknik när man jämför böcker är att berätta för jämförelseverktyget att försköna text- och formatfiler innan man beräknar skillnader. Detta kan ofta resultera i renare och lättare att följa skillnader. För att göra detta, klicka på knappen Alternativ längst ner till höger och välj Försköna filer innan man jämför dem. Observera att förskönande ibland kan ha oönskade effekter, eftersom det kan göra att ogiltig markering ändras för att göra den giltig. Du kan också ändra antalet kontextrader som visas runt skillnader via knappen Alternativ.

Du kan söka efter all text i skillnaderna via sökfältet längst ner. Du måste ange vilken panel att söka i, vänster eller höger.

Starta jämförelseverktyget

Jämförelseverktyget är mest användbart när du har två versioner av samma bok och du vill se vad som skiljer mellan dem. För detta ändamål finns det flera sätt att starta verktyget.

Jämför två e-bokfiler

Öppna den första filen i verktyget Redigera e-böcker. Klicka nu på Arkiv → Jämför med en annan bok och välj den andra filen (måste vara i samma format som den första). Jämförelsevyn öppnas med filen som redigeras till höger och den andra filen till vänster.

Jämföra ORIGINAL_FMT med FMT

När du gör en konvertering i calibre från en FMT till sig själv sparas originalfilen som ORIGINAL_FMT. Du kan se vad som har ändrats genom konverteringen genom att högerklicka på ORIGINAL_FMT-posten i Bokdetaljer-panelen i calibre-huvudfönstret och välja Jämför med FMT. Jämförelsevyn öppnas med ORIGINAL_FMT till vänster och FMT till höger.

Jämföra en kontrollpunkt med bokens aktuella tillstånd under redigering

Verktyget Redigera e-böcker har en mycket användbar funktion som kallas Kontrollpunkter. Detta gör att du kan spara bokens aktuella status som en namngiven kontrollpunkt, som du kan återgå till om du inte gillar ändringarna du har gjort sedan kontrollpunkten skapades. Kontrollpunkter skapas också automatiskt när du utför olika automatiserade åtgärder i redigeraren. Du kan se listan över kontrollpunkter genom att gå till Visa → Kontrollpunkter och sedan använda knappen Jämför för att jämföra boken vid den valda kontrollpunkten med det aktuella tillståndet. Jämförelseverktyget visar kontrollpunkten till vänster och aktuell status till höger.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Redigera metadata för e-böcker

Redigera metadata för e-böcker

Innehåll

- Redigera metadata för en bok i taget

- Hämta metadata

- Hantera bokformat

- Allt om omslag

- Redigera metadata för många böcker samtidigt

- Sök och ersätt

- Masshämtning av metadata

- Lägga till extra datafiler i en bok

E-böcker finns i alla former och storlekar, och oftare än inte, deras metadata (saker som titel/författare/serie/utgivare) är ofullständig eller felaktig. Det enklaste sättet att ändra metadata i calibre är att helt enkelt dubbelklicka på en post och skriv in rätt ersättning. För mer sofistikerade, ”kraftig redigering” använd metadataredigeringsverktygen som diskuteras nedan.

Redigera metadata för en bok i taget

Klicka på den bok du vill redigera och klicka sedan på knappen Redigera metadata eller tryck på nyckeln E. En dialogruta öppnas som låter dig redigera alla aspekter av metadata. Den har olika funktioner för att göra redigeringen snabbare och effektivare. En lista med de vanligaste tipsen:

- Du kan klicka på knappen mellan titel och författare för att byta ut dem automatiskt.

- Du kan klicka på knappen bredvid författarsortering för att calibre automatiskt ska fylla i den med hjälp av de sorteringsvärden som lagrats med varje författare. Använd dialogrutan Hantera författare för att se och ändra författarnas sorteringsvärden. Denna dialogruta kan öppnas genom att klicka och hålla ned knappen bredvid författarsortering.

- Du kan klicka på knappen bredvid taggar för att använda taggredigerare för att hantera taggar som är kopplade till boken.

- Rutan ”identifierare” kan användas för att mata in ett ISBN (och många andra typer av id), den kommer att ha en röd bakgrund om du anger ett ogiltigt ISBN. Den kommer att vara grönt för giltiga ISBN-nummer.

- Författarsorteringsrutan blir röd om författarens sorteringsvärde skiljer sig från vad calibre tror att det borde vara.

Hämta metadata

Den trevligaste funktionen i dialogrutan redigera metadata är dess förmåga att automatiskt fylla i många metadatafält genom att få metadata från olika webbplatser. För närvarande använder calibre Google Böcker och Amazon. Metadata-hämtningen kan fylla i titel, författare, serier, taggar, betyg, beskrivning och ISBN för dig.

För att använda hämtningen, fyll i titel- och författarfälten och klicka på knappen Hämta metadata. calibre kommer att presentera en lista med böcker som bäst matchar titeln och författaren. Om du först fyller i ISBN-fältet kommer det att användas framför titel och författare. Om inga träffar hittas, försök att göra din sökning lite mindre specifik genom att bara ta med några nyckelord i titeln och bara författarens efternamn.

Hantera bokformat

I calibre kan en enda bokpost ha många olika format associerade med den. Till exempel kan du ha fått hela verk av Shakespeare i EPUB-format och senare konverterade den till MOBI att läsa på din Kindle. calibre hanterar automatiskt flera format för dig. I avsnittet Tillgängliga format i dialogrutan Redigera metadata kan du hantera dessa format. Du kan lägga till ett nytt format, ta bort ett befintligt format och även be calibre för att ställa in metadata och omslag för bokposten från metadata i ett av formaten.

Allt om omslag

Du kan be calibre hämtar bokomslag för dig, förutsatt att boken har ett känt ISBN. Alternativt kan du ange en fil på din dator som ska användas som omslag. calibre kan även skapa ett standardomslag med grundläggande metadata för dig. Du kan dra och släppa bilder på omslaget för att byta det och även högerklicka för att kopiera/klistra in omslagsbilder.

Dessutom finns det en knapp för att automatiskt beskära kanter från omslaget, ifall att din omslagsbild har en ful kant.

Redigera metadata för många böcker samtidigt

Välj först de böcker du vill redigera genom att hålla ner Ctrl eller Skift och klicka på dem. Om du väljer mer än en bok, klicka på knappen Redigera metadata kommer att öppna Mass-metadataredigeringsdialogrutan. Med den här dialogrutan kan du snabbt ställa in författare/utgivare/betyg/taggar/serier o.s.v. av ett gäng böcker till samma värde. Detta är särskilt användbart om du just har importerat ett antal böcker som har någon metadata gemensamt. Den här dialogrutan är mycket kraftfullt, till exempel har den fliken Sök och ersätt som du kan använda för att utföra massoperationer på metadata och med kopiera metadata från en kolumn till en annan.

Normala redigeringsdialogrutan för metadata har också knapparna Nästa- och Föregående som du kan använda för att redigera metadata för flera böcker efter varandra.

Sök och ersätt

Dialogrutan Redigera metadata för många böcker låter dig utföra godtyckligt kraftfulla sök- och ersättningsåtgärder på de valda böckerna. Som standard använder den en enkel textsökning och ersättning, men den stöder också reguljära uttryck. För mer om reguljära uttryck, se Allt om att använda reguljära uttryck i calibre.

Som nämnts ovan finns det två sök- och ersättningslägen: teckenmatchning och reguljärt uttryck. Teckenmatchning kommer att se i sökfältet som du väljer för de tecken du skriver i rutan sök efter och ersätta dessa tecken med det du skriver i rutan ersätt med. Varje förekomst av söktecken i fältet kommer att ersättas. Antag till exempel att fältet som du söker innehåller a bad cat. Om du söker efter a som ska ersättas med HELLO, blir resultatet HELLO bHELLOd cHELLOt.

Om fältet du söker på är ett fler-fält som taggar, behandlas varje tagg separat. Till exempel om dina taggar innehåller skräck, kuslig, kommer sökuttrycket r inte matcha något eftersom uttrycket först kommer att tillämpas på skräck och sedan på kuslig.

Om du vill att sökningen ska ignorera skillnader mellan versaler och gemener, avmarkera rutan Skiftlägeskänslig.

Du kan låta calibre ändra skiftläget i resultatet (information efter det att ersättning har hänt) genom att välja en av funktionerna från rutan Tillämpa funktion efter ersättning. De tillgängliga operationerna är:

- Lower case – ändra alla tecken i fältet till gemener

- Upper case – ändra alla tecken i fältet till versaler

- Titel case – använd versaler för varje ord i resultatet.

Rutan Ditt test tillhandahålls för att du ska kunna skriva in text för att kontrollera att sök/ersätt gör vad du vill. I de flesta fall kommer boktestrutorna att räcka, men det är möjligt att det finns ett fall du vill kontrollera som inte visas i dessa rutor. Skriv in det fallet i ”Ditt test”.

Reguljära uttrycksläget har vissa skillnader från teckenläge, förutom (naturligtvis) användning av reguljära uttryck. Den första är att funktionerna tillämpas på de delar av strängen som matchar söksträngen, inte hela fältet. Det andra är att funktionerna gäller ersättningssträngen, inte hela fältet.

Den tredje och viktigaste är att ersättsträngen kan referera till delar av söksträngen genom att använda bakåtreferenser. En bakåtreferens är \\n där n är ett heltal som refererar till den n:te gruppen inom parentes i sökuttrycket. Till exempel, ges samma exempel som ovan, a bad cat, ett sökuttryck a (…) (…), och ett ersättuttryck a \2 \1, kommer resultatet att vara a cat bad. Se Allt om att använda reguljära uttryck i calibre för mer information om bakåtreferenser.

Ett användbart mönster: anta att du vill ändra skiftläge för ett helt fält. Det enklaste sättet att göra detta är att använda teckenläge, men låt oss vidare anta att du vill använda reguljärt uttrycksläge. Sökuttrycket bör vara (^.*$), ersättuttrycket bör vara \1 och önskad skiftlägesändringsfunktion bör väljas.

Slutligen kan du i reguljära uttrycksläget kopiera värden från ett fält till ett annat. Gör helt enkelt käll- och destinationsfältet annorlunda. Kopian kan ersätta destinationsfältet, lägg till i fältet (i början eller slutet). Kryssrutan ”använd kommatecken” berättar för calibre att (eller inte) lägga till ett kommatecken mellan texten och destinationsfältet för lägg till i början och slutet lägena. Om destinationen är flera (t.ex. taggar) kan du inte avmarkera den här rutan.

Sök och ersätt görs efter att alla andra metadataändringar i de andra flikarna har tillämpats. Det här kan leda till viss förvirring, eftersom testrutorna visar informationen före de andra ändringarna, men åtgärden kommer att tillämpas efter de andra ändringarna. Om du har några tvivel om vad som kommer att hända, blanda inte sök/ersätt med andra ändringar.

Masshämtning av metadata

Om du vill hämta metadata för flera böcker samtidigt, högerklicka på knappen Redigera metadata och välj Hämta metadata. Du kan välja att endast hämta metadata, omslag, eller båda.

Lägga till extra datafiler i en bok
calibre kan lagra valfritt antal extra datafiler kopplade till en bok. Dessa kan vara alternativa omslag, tilläggsmaterial o.s.v. De kan inte ses direkt eller användas som konverteringskällor. Inte heller indexeras de av fulltextsökmotorn i calibre. För att visa/lägga till/ta bort dem, välj boken och högerklicka på knappen Redigera metadata och välj Hantera datafiler. Det här kommer att öppna ett fönster där du kan utföra operationer på dessa filer. Alternativt kan du högerklicka på knappen Lägg till böcker och välja Lägg till datafiler till valda bokposter för att snabbare lägga till datafiler.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Vanliga frågor

Vanliga frågor

Innehåll

- Konvertering av e-bokformat

- Enhetsintergration

- Bibliotekshantering

- Diverse

Konvertering av e-bokformat

Innehåll

- Vilka format stöder calibre-konvertering till/från?

- Vilka är de bästa källformaten att konvertera?

- Jag konverterade en PDF-fil, men resultatet har diverse problem?

- Hur konverterar jag min fil som innehåller icke-engelska tecken eller typografiska citattecken?

- Vad är det med innehållsförteckningar i MOBI-filer?

- Hur konverterar jag en samling HTML-filer i en viss ordning?

- EPUB:en som producerades med calibre är inte giltig?

- Hur använder jag de avancerade funktionerna i konverteringsverktyget?

Vilka format stöder calibre-konvertering till/från?

calibre stöder konvertering av många inmatningsformat till många utmatningsformat. Den kan konvertera alla inmatningsformat i följande list, till alla utmatningsformat.

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

Utmatningsformat: AZW3, EPUB, DOCX, FB2, HTMLZ, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

Observera

PRC är ett generiskt format, calibre stöder PRC-filer med TextRead och MOBIBook-sidhuvuden. PDB är också ett generiskt format. calibre stöder eReader, Plucker (endast inmatning), PML och zTxt PDB-filer. DJVU-stöd är endast för att konvertera DJVU-filer som innehåller inbäddad text. Dessa skapas vanligtvis av OCR-mjukvara. MOBI-böcker kan vara av två typer Mobi6 och KF8. calibre stöder båda till fullo. MOBI-filer har ofta filändelserna .azw eller .azw3. DOCX-filer från Microsoft Word 2007 och senare stöds.

Vilka är de bästa källformaten att konvertera?

I fallande preferens: LIT, MOBI, AZW, EPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

Jag konverterade en PDF-fil, men resultatet har diverse problem?

PDF är ett hemskt format att konvertera från. För en lista med diverse problem du kommer stöta på vid PDF konvertering, se: Konvertera PDF-dokument.

Hur konverterar jag min fil som innehåller icke-engelska tecken eller typografiska citattecken?

Det finns två aspekter på detta problem:

1. Att känna till källfilens kodning: calibre försöker uppskatta vilken teckenkodning dina källfiler använder, men ofta är det omöjligt, så du måste tala om vilken kodning som ska användas. Det här kan göras i det grafiska gränssnittet via fältet Teckenkodning för inmatning i avsnittet Utseende & känsla → Text av konverteringsdialogrutan. Kommandoradsverktygen har ett ebook-convert-txt-input --input-encoding alternativ.

2. När du lägger till HTML-filer till calibre, kan du behöva tala om för calibre vilken kodning filerna har. För att göra detta går du till Inställningar → Avancerat → insticksmoduler → Filtyp och anpassa insticksmodulen HTML till ZIP, för att berätta vad kodning dina HTML-filer har. Nu när du lägger HTML-filer till calibre de kommer att behandlas på rätt sätt. HTML-filer från olika källor och har ofta olika kodningar, så du kan behöva ändra denna inställning upprepade gånger. En gemensam kodning för många filer från webben är cp1252 och jag föreslår att du testar det först. Observera att när du konverterar HTML-filer, lämna ovan nämnda kodningsinställningen för indata tom. Detta beror på att insticksmodulen HTML till ZIP konverterar automatiskt HTML-filerna till en standardkodning (UTF-8).

Vad är det med innehållsförteckningar i MOBI-filer?

Det första att inse är att de flesta e-böcker har två innehållsförteckningar. Den ena är den traditionella innehållsförteckningen, som innehållsförteckningen du hittar i pappersböcker. Den här innehållsförteckningen är en del av huvuddokumentflödet och kan utformas som du vill. Denna innehållsförteckning kallas innehållets innehållsförteckningen.

Sedan är det metadatainnehållsförteckningen. Metadatainnehållsförteckningen är en innehållsförteckning som inte är en del av bokens text och vanligtvis nås av någon speciell knapp på en läsenhet. Till exempel i calibres e-bokvisare använder du Visa innehållsförteckningen för att se den här innehållsförteckningen. Denna innehållsförteckningen kan inte anpassas av bokens skapare. Hur det representeras är upp till läsarprogrammet.

I MOBI-formatet är situationen lite förvirrad. Detta beror på att MOBI-formatet, ensamt bland vanliga e-bokformat, inte har ordentlig stöd för ett metadatainnehållsförteckning. En MOBI-bok simulerar närvaron av en metadatainnehållsförteckning genom att sätta en extra innehållsförteckning i slutet av boken. När du klickar på Gå Innehållsförteckning på din Kindle, är det denna extra Innehållsförteckning Kindle tar dig.

Nu kan det tyckas att MOBI-boken har två identiska innehållsförteckningar. Kom ihåg att en är semantiskt en innehållsförteckning och den andra är en metadatainnehållsförteckning, även om båda kan ha exakt samma poster och ser likadana ut. Man kan nå den ena direkt från Kindle-menyerna, men inte den andra.

Vid konvertering till MOBI upptäcker calibre metadatainnehållsförteckning i inmatningsdokumentet och skapar en slut-på-fil-innehållsförteckning i MOBI-utmatningsfilen. Du kan stänga av detta med ett alternativ i inställningarna för MOBI-utmatning. Du kan också berätta för calibre att lägga den i början eller slutet av boken via ett alternativ i inställningarna för MOBI-utmatning. Kom ihåg att denna innehållsförteckning är semantiskt en metadata-innehållsförteckning, i något annat format än MOBI kan den inte vara en del av texten. Det faktum att den är en del av texten i MOBI är en olycka som orsakats av begränsningar i MOBI. Om du vill ha en innehållsförteckning på en viss plats i din dokumenttext skapar du en för hand. Så vi rekommenderar starkt att du lämnar standardvärdet som det är, det vill säga med metadatainnehållsförteckningen i slutet av boken. Observera också att om du inaktiverar skapandet av slut-på-fil-innehållsförteckning kanske den resulterande MOBI-filen inte fungerar korrekt på en Kindle, eftersom Kindle använder metadatainnehållsförteckningen för många saker, inklusive sidvändningsfunktionen.

Om du har en handredigerad innehållsförteckning i inmatningsdokumentet kan du använda alternativen för innehållsförteckningsupptäckt i calibre för att automatiskt skapa metadatainnehållsförteckning från den. Se konverteringsavsnittet i användarmanualen för mer information om hur du använder de här alternativen.

Slutligen uppmuntrar jag dig att avstå från innehållsförteckningen och bara ha en metadatainformation i dina e-böcker. Metadatainnehållsförteckningen kommer att ge människor som läser dina e-böcker en mycket överlägsen navigeringsupplevelse (förutom på Kindle, där de är i huvudsak samma som en innehållsförteckningen).

Observera

Det nyare AZW3-formatet har ordentligt stöd för metadatainnehållsförteckning. Emellertid tenderar Kindle-mjukvaran inte fungera om du inaktiverar skapande av slut-på-fil-indragsinnehållsförteckning. Så det rekommenderas att du lämnar den skapade innehållsförteckningen som den är. Om du skapar en AZW3-fil med metadatainnehållsförteckning och ingen slut-på-fil skapad innehållsförteckning, kommer vissa funktioner på Kindle inte att fungerar, till exempel sidväxlingsfunktionen.

Hur konverterar jag en samling HTML-filer i en viss ordning?

För att konvertera en samling HTML-filer i en viss ordning, måste du skapa en innehållsförteckningsfil. Det vill säga, en annan HTML fil som innehåller länkar till de andra filerna i önskad ordning. En sådan fil ser ut som:

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

Lägg sedan till den här HTML-filen i det grafiska gränssnittet och använd knappen Konvertera för att skapa din e-bok. Du kan använda alternativet i innehållsförteckningsavsnittet i konverteringsdialogrutan för att anpassa hur innehållsförteckningen genereras.

Observera

Som standard när du lägger till HTML-filer, följer calibre länkar i filerna i djup först ordning. Detta innebär att om filen A.html länkar till B.html och C.html och D.html, men B.html länkar också till D.html, då kommer filerna att vara i ordning A.html, B.html, D Html, C.html. Om du istället vill att för att ordningen ska vara A.html, B.html, C.html, D.html måste du berätta för calibre att lägga dina filer i bredden först ordning. Gör detta genom att gå till Inställningar → Avancerat → Insticksmoduler → Filtyp och anpassa HTML till ZIP-insticksmodulen.

EPUB:en som producerades med calibre är inte giltig?

calibre garanterar inte att en EPUB-fil som produceras är giltigt. Den enda garantin den gör är att om du matar i den giltigt XHTML 1.1 + CSS 2.1 så kommer den att mata ut en giltig EPUB-fil. calibre anstränger sig hårt för att säkerställa att de EPUB-filer som produceras faktiskt fungerar som det är tänkt på en mängd olika enheter, ett mål som inte är förenligt med att producera giltiga EPUB-filer, och ett som är mycket viktigare för de allra flesta användare. Om du behöver ett verktyg som alltid producerar giltiga EPUB-filer är calibre inte för dig. Detta innebär att om du vill skicka en calibre-producerad EPUB-fil till en webbutik som använder en EPUB-giltighetskontrollerare, måste du själv se till att EPUB-filen är giltig, calibre gör det inte åt dig – med andra ord måste du mata calibre med giltigt XHTML + CSS som inmatningsdokument.

Hur använder jag de avancerade funktionerna i konverteringsverktyget?

Du kan få hjälp med vilken enskild funktion som helst av konverteringarna genom att föra muspekaren över den i det grafiska gränssnittet eller köra ebook-convert dummy.html .epub -h i en terminal. Ett bra ställe att börja är att titta på följande demofil som visar några av de avancerade funktionerna html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

Enhetsintergration

Innehåll

- Vilka enheter stöder calibre?

- Hur kan jag hjälpa till för att min enhet ska få stöd i calibre?

- Min enhet upptäcks inte av calibre?

- Min enhet är inte standard eller ovanlig. Vad kan jag göra för att ansluta till den?

- Hur använder jag calibre med min iPad/iPhone/iPod touch?

- Hur använder jag calibre med min Android-telefon/surfplatta eller Kindle Fire?

- Kan jag komma åt mina calibre-böcker med hjälp av webbläsaren i min Kindle eller annan läsenhet?

- Jag kan inte sända e-post med hjälp av calibre?

- Min enhet monteras som skrivskyddad i Linux, så calibre kan inte ansluta till den?

- Varför stöder inte calibre samlingar på Kindle eller hyllor på Nook?

- Jag får ett felmeddelande när jag försöker använda calibre med min Kobo Touch/Glo/osv.?

- Omslag för böcker som jag överför till min e-bläck Kindle dyker upp en stund och ersätts sedan av ett generiskt omslag?

- Omslagen för mina MOBI-filer har slutat visas i Kindle för PC/Kindle for Android/iPad osv.

- Jag överförde några böcker till min Kindle genom att använda calibre och de dök inte upp?

Vilka enheter stöder calibre?

calibre kan direkt ansluta till alla de stora (och de flesta av de mindre) e-bokläsenheterna, smarta mobiler, surfplattor osv. Dessutom kan du använda funktionen Anslut till mapp med valfri e-bokläsenhet som exporterar sig själv som en USB-disk. Slutligen kan du ansluta trådlöst till någon enhet som har en webbläsare med hjälp av calibre-innehållsservern.

Hur kan jag hjälpa till för att min enhet ska få stöd i calibre?

Om din enhet visas som en USB-disk för operativsystemet, att lägga till stöd för calibre är mycket enkelt. Vi behöver bara lite information från dig:

- Komplett lista med e-bokformat som din enhet stöder.

- Finns det en särskild mapp på enheten där alla e-bokfiler ska placeras? Också om inte enheten upptäcker filer som placeras i undermappar?

- Vi behöver också information om din enhet som calibre samlar in automatiskt. Först, om din enhet har stöd för SD-kort, sätt in dem. Anslut sedan din enhet till datorn. I calibre gå till Inställningar → Diverse och klicka på knappen ”Felsök enhetsdetektering”. Detta kommer att skapa en del felsökningsutskrifter. Kopiera den till en fil och upprepa processen, den här gången med din enhet frånkopplad från din dator.

- Sänd båda ovanstående utmatning till oss med andra uppgifter och vi kommer att skriva en drivrutin för din enhet.

När du skickar oss utmatningen för ett visst operativsystem, kommer stöd för enheten i det operativsystemet att visas i nästa version av calibre. För att skicka utmatningen till oss, öppna en felanmälan och utmatningen utdata till den. Se hur man gör felanmälningar [https://calibre-ebook.com/sv/bugs].

Min enhet upptäcks inte av calibre?

Följ dessa steg för att hitta problemet:

- Se till att du bara ansluter en enda enhet till din dator åt gången. Använd inte en annan enhet som stöds av calibre som en iPhone/iPad osv. samtidigt.

- Om du ansluter en Apple iEnhet (iPad, iPod Touch, iPhone), Apple tillåter inte längre program från tredje part att ansluta till sina enheter med en USB-kabel. Använd i stället en trådlös anslutning via calibre-innehållsservern.

- Om du ansluter en Kindle Fire eller annan Android-enhet, läs anteckningen under Med hjälp av en USB-kabel.

- På macOS om du får behörighetsfel när du ansluter en enhet till calibre kan du åtgärda det genom att titta under Systeminställningar > Säkerhet och sekretess > Sekretess > Filer och mappar.

- Se till att du kör den senaste versionen av calibre (för närvarande 7.10.0). Den senaste versionen kan alltid hämtas från calibres webbplats [https://calibre-ebook.com/sv/download]. Du kan se vilken version av calibre du kör för närvarande genom att titta på den nedre raden av calibres huvudfönster.

- Försäkra att ditt operativsystem ser enheten. Det vill säga, enheten ska visas i utforskaren (i Windows) eller Finder (i macOS).

- I calibre, gå till Inställningar → Ignorerade enheter och kontrollera att din enhet inte ignoreras

- Om alla ovanstående steg misslyckas går du till Inställningar → Diverse och klickar på Felsök enhetsdetektering med din enhet ansluten och lägg upp utmatningen som ett ärende på felhanteringssystemet för calibre [https://bugs.launchpad.net/calibre].

Min enhet är inte standard eller ovanlig. Vad kan jag göra för att ansluta till den?

Förutom funktionen Anslut till mapp under knappen Ansluta/dela, ger calibre en Användardefinerad-enhetsinsticksmodul som kan användas för att ansluta till en USB-enhet som dyker upp som en hårddisk i operativsystemet. Observera: Windows, måste enheten ha en enhetsbeteckning för calibre ska kunna använda den. Se enhets insticksmodulen Inställningar->Insticksmoduler->Enhets insticksmodul->Användardefinerad och Inställningar->Diverse->Få informationen för att ställa in användardefinierade enhet för mer information. Observera att om du använder det användardefinierade insticksmodulen för en enhet som normalt upptäcks av en inbyggd calibre-insticksmodul, måste du stänga av den inbyggda insticksmodulen först, så att din användardefinierade insticksmodul används istället.

Hur använder jag calibre med min iPad/iPhone/iPod touch?

Ett enkelt sätt att bläddra i din calibre-samling från din Apple-enhet är att använda calibre-innehållsservern, som gör din samling tillgänglig över nätet. Utför först följande steg i calibre

- Ställ önskat utmatningsformat i calibre till EPUB (Utmatningsformatet kan ställas in under Inställningar → Gränssnitt → Beteende)

- Ställ in utmatningsprofil till iPad (detta kommer att fungera för iPhone/iPod också), enligt Inställnigar → Konvertera → Vanliga alternativ → Sidinställning

- Konvertera de böcker du vill läsa på din iEnhet till EPUB-format genom att välja dem och klicka på knappen Konvertera.

- Slå på innehållsservern genom att klicka på knappen Ansluta/dela och lämna calibre igång. Du kan även tala om för calibre att automatiskt starta innehållsservern via Inställningar → Delning → Dela via nätet.

Innehållsservern låter dig läsa böcker direkt i Safari. Dessutom finns det många appar för din iEnhet som kan anslutas till calibre-innehållsservern. Exempel är: Marvin, Mapleread och iBooks.

Använda innehållsservern

Starta webbläsaren Safari och skriv in IP-adressen och porten på den dator som kör calibre-servern, så här:

http://192.168.1.2:8080/

Ersätt 192.168.1.2 med den lokala IP-adressen på datorn som kör calibre. Se calibre-innehållsservern för detaljer om hur du kör servern och tar reda på rätt IP-adress att använda.

Du kommer att se en lista med böcker i Safari, tryck på en bok och du kommer att få möjligheten att hämta den, eller läsa den i själva webbläsaren. Om du vill hämta den frågar Safari om du vill öppna den med iBooks.

Många läsningsappar stöder att bläddra i calibre-biblioteket direkt via dess stöd för OPDS. I sådana appar kan du gå till katalogskärmen för nätet och lägga till IP-adressen för calibre-servern för att bläddra och hämta böcker från ditt calibre-bibliotek i appen.

Hur använder jag calibre med min Android-telefon/surfplatta eller Kindle Fire?

Det finns två sätt som du kan ansluta din Android-enhet till calibre. Med hjälp av en USB-kabel – eller trådlöst, via etern. Det första steget till att använda en Android-enhet är att installerar en e-bok att läsa program på den. Det finns många gratis och betal e-bokläsningsapplikationer för Android: Några exempel (utan inbördes ordning): 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].

Med hjälp av en USB-kabel

Anslut helt enkelt din enhet till datorn med en USB-kabel. calibre ska automatiskt upptäcka enheten och sedan kan du överföra böcker till den genom att klicka på knappen Överför till enhet. Observera att på macOS och Linux kan endast ett enda program ansluta till en Android-enhet åt gången, så se till att enheten inte öppnas i OS-filhanteraren eller Android-filöverföringsverktyget osv.

Observera

Med nyare Android-enheter kan du behöva gå igenom några hinder för att få anslutningen att fungera, eftersom Google verkligen inte vill att du ska vara oberoende av dess moln. Lås först upp skärmen innan du ansluter USB-kabeln. När du ansluter USB-kabeln får du en popup-avisering. Se till att det står något som ”Överföra mediefiler” eller ”MTP (Media Transfer Mode)”. Om det inte gör det, tryck på aviseringen och ändra läget till Media Transfer (MTP). Du kan behöva starta om calibre vid denna tidpunkt för att din enhet ska kännas igen. Slutligen, kan du få en popup i enheten varje gång calibre eller operativsystemet försöker ansluta till den, och ber om tillåtelse, tryck på OK.

Observera

Med Kindle Fire 8 eller senare finns det en ikon som visas när USB-kabeln är ansluten, vilket visar att enheten laddas. Tryck på den och växla enheten till dataöverföringsläge och starta sedan calibre, den bör då upptäckas.

Över luften
calibre har en inbyggd webbserver, calibre-innehållsservern. Den gör din calibre-samling tillgänglig över nätet. Du kan bläddra i den på din enhet med en enkel webbläsare eller en dedikerad applikation. Utför först följande steg i calibre:

- Ställ in Önskat utmatningsformat i calibre till EPUB för normala Android-enheter eller MOBI för Kindle-läsenheter (Utmatningsformatet kan ställas in under Inställningar → Gränssnitt → Beteende)

- Konvertera de böcker du vill läsa på din iEnhet till EPUB-format genom att välja dem och klicka på knappen Konvertera.

- Slå på innehållsservern i calibres inställningar och lämna calibre igång.

Öppna nu webbläsaren på din Android-enhet och bläddra till

http://192.168.1.2:8080/

Ersätt 192.168.1.2 med den lokala IP-adressen på datorn som kör calibre. Se calibre-innehållsservern för detaljer om hur du kör servern och tar reda på rätt IP-adress att använda.

Du kan nu bläddra i din boksamling och hämta böcker från calibre till din enhet för att öppna med någon e-bokläsningsmjukvara du har på din Android-enhet.

Många läsningsappar stöder att bläddra i calibre-biblioteket direkt via dess stöd för OPDS. I sådana appar kan du gå till katalogskärmen för nätet och lägga till IP-adressen för calibre-servern för att bläddra och hämta böcker från ditt calibre-bibliotek i appen.

Kan jag komma åt mina calibre-böcker med hjälp av webbläsaren i min Kindle eller annan läsenhet?

calibre har en innehållsserver som exporterar böckerna i calibre som en webbsida. Se calibre-innehållsservern för detaljer.

Vissa enheter, som Kindle (1/2/DX), tillåter dig inte att komma åt port 8080 (standardporten som innehållsservern körs på). Ändra i så fall porten i inställningarna för calibre till 80. (På vissa operativsystem kanske du inte kan köra servern på ett portnummer mindre än 1024 på grund av säkerhetsinställningar. I det här fallet är den enklaste lösningen att justera din router för att vidarebefordra förfrågningar på port 80 till port 8080).

Dessutom har vissa enheter inte tillräckligt avancerade webbläsare för att köra det appliknande gränssnittet som används av innehållsservern. För sådana enheter kan du helt enkelt lägga till /mobile i serverns URL för att använda ett förenklat gränssnitt utan JavaScript.

Jag kan inte sända e-post med hjälp av calibre?

På grund av den stora mängden skräppost i e-post kan det vara svårt att skicka e-post, eftersom olika e-postservrar använder olika strategier för att blockera e-post. Det vanligaste problemet är om du skickar e-post direkt (utan e-postreläserver) i calibre. Många servrar (till exempel Amazon) blockerar e-post som inte kommer från en välkänt reläserver. Det mest robusta sättet att ställa in e-postsändning i calibre är att göra följande:

- Skapa ett gratis GMX-konto på GMX [https://www.gmx.com].

- Gå till Inställningar → Delning → Dela böcker via e-post i calibre och klicka på knappen Använd GMX och fyll i den information som efterfrågas.

- Logga in på ditt GMX-konto på webbplatsen och aktivera SMTP-sändning (Inställningar->POP3 & IMAP->Skicka och ta emot e-post via externt program)

- calibre kommer då att kunna använda GMX för att skicka meddelandet.

- Om du skickar till din Kindle, kom ihåg att uppdatera e-postinställningarna på din Amazon Kindle-sida så att e-post skickas från din GMX-e-postadress. Observera också att Amazon inte tillåter e-postleverans av AZW3- och MOBI-filer i nytt format (KF8). Slutligen började Amazon nyligen skicka bekräftelse-e-postmeddelanden som du måste klicka på tillbaka till ditt GMX-konto innan boken faktiskt levereras.

Även efter att ha gjort detta, kan du få problem. En vanlig källa till problem är att vissa dåligt utformade antivirusprogram blockerar calibre från att öppna en anslutning för att sända e-post. Testa att lägga ett undantag för calibre i ditt antivirusprogram.

Observera

Microsoft/GMX kan inaktivera ditt konto om du använder det för att skicka stora mängder e-post. Så när du använder dessa tjänster för att skicka e-post begränsar calibre sig automatiskt till att skicka en bok var femte minut. Om du inte har något emot att riskera att ditt konto blockeras kan du minska detta fördröjningsintervall genom att gå till Inställningar → Avancerat → Justeringar i calibre.

Observera

Google bröt nyligen medvetet deras e-postsändningsprotokoll (SMTP) i ett försök att tvinga alla att använda deras webbgränssnitt så att de kan visa dig fler annonser. De försöker hävda att SMTP är osäkert, det är felaktigt och helt enkelt en ursäkt. Använd någon annan e-postleverantör istället.

Observera

Om du är orolig för att ge calibre åtkomst till ditt e-postkonto, skapa helt enkelt ett nytt gratis e-postkonto med GMX eller Outlook och använd det endast för calibre.

Min enhet monteras som skrivskyddad i Linux, så calibre kan inte ansluta till den?

Linux-kärnor monterar enheter skrivskyddade när deras filsystem har fel. Du kan reparera filsystemet med:

sudo fsck.vfat -y /dev/sdc

Ersätt /dev/sdc med sökvägen till enhetsnoden på din enhet. Du hittar enhetsnoden av enheten, som alltid kommer att vara under /dev genom att undersöka utmatning av:

mount

Varför stöder inte calibre samlingar på Kindle eller hyllor på Nook?

Varken Kindle eller Nook ger något sätt att manipulera samlingar över en USB-anslutning. Om du verkligen bryr dig om att använda samlingar vill jag uppmana dig att sälja din Kindle/Nook och skaffa en Kobo. Endast Kobo verkar förstå att livet är för kort för att skriva in samlingarna en efter en på en e-bläckskärm 😇

Observera att i fallet med Kindle, finns det ett sätt att manipulera samlingar via USB, men det kräver att Kindle startas om varje gång den kopplas bort från datorn, för ändringar i samlingarna ska erkännas. Som sådan, är det osannolikt att någon calibre-utvecklare någonsin kommer att känna sig motiverade nog att stöda det. Det finns dock en calibre-insticksmodul som låter dig skapa samlingar på din Kindle från calibre-metadata. Den är tillgänglig härifrån [https://www.mobileread.com/forums/showthread.php?t=244202].

Observera

Amazon har tagit bort möjligheten att manipulera samlingar helt och hållet i sina nyare modeller, som Kindle Touch och Kindle Fire, vilket gör även ovanstående insticksmodul värdelös, om du inte ”rootar” din Kindle och installerar anpassad mjukvara.

Jag får ett felmeddelande när jag försöker använda calibre med min Kobo Touch/Glo/osv.?

Den Kobo har mjukvara med mycket fel. Anslutning till den har varit känt att slumpmässigt misslyckas. Vissa kombinationer av moderkort, USB-portar/kablar/hubbar kan förvärra denna tendens att misslyckas. Om du får ett felmeddelande när du ansluter till din beröring med calibre testa följande, som var och en har löst problemet för vissa calibre-användare.

- Anslut Kobo direkt till din dator, inte via USB-hubb

- Försök med en annan USB-kabel och en annan USB-port på din dator

- Logga ut från Kobo och logga in igen, detta gör att databasen byggs om, reparerar korrupta databasfel.

- Försök uppgradera mjukvaran på din Kobo Touch till den senaste

- Testa att återställa Kobo (ibland botar problemet för ett tag, men sedan visas på nytt, i vilket fall du måste återställa igen och igen)

- Försök att endast lägga en eller två böcker på Kobo åt gången och ha inte stora samlingar på Kobo

Omslag för böcker som jag överför till min e-bläck Kindle dyker upp en stund och ersätts sedan av ett generiskt omslag?

Detta händer på grund av ett Amazon-fel. De försöker hämta ett omslag till boken från sina servrar och när det misslyckas ersätter de det befintliga omslaget som calibre skapade med ett generiskt omslag. För detaljer, se denna forumtråd [https://www.mobileread.com/forums/showthread.php?t=329945]. Från version 4.17 har calibre en lösning, där om du ansluter Kindle till calibre efter att omslagen har förstörts av Amazon, kommer calibre återställa dem automatiskt. Så för att se omslagen på din Kindle, måste du:

1. Överför boken till Kindle med calibre

2. Koppla bort Kindle och vänta på att Amazon förstör omslaget

3. Återanslut Kindle till calibre

Observera att den här lösningen endast fungerar för böcker som överförs med calibre 4.17 eller nyare. Alternativt kan du helt enkelt hålla din Kindle i flygläge, du vill inte att Amazon känner till alla böcker som du läser ändå. Jag uppmuntrar dig att kontakta Amazons kundtjänst och klaga högt om detta fel. Kanske kommer Amazon att lyssna.

Observera

Om lösningen inte fungerar för dig måste du kontrollera att Kindle-mjukvaran åtminstone är version 5.12.5, släppt i april 2020.

Omslagen för mina MOBI-filer har slutat visas i Kindle för PC/Kindle for Android/iPad osv.

Detta orsakas av ett fel i Amazon-programmet. Du kan lösa det genom att gå till Inställningar → Konvertera → Alternativ för utmatning → MOBI utmatning och ställa in Aktivera delning av bokinnehåll alternativet. Om du kodar om en tidigare konverterade bok, måste du också aktivera alternativet i konverteringsdialogrutan för att enskild bok (som per bok konverteringsinställningar sparas och har företräde).

Observera att detta kommer att innebära att den skapade MOBI kommer att dyka upp i personliga dokument istället för böcker på Kindle Fire och Amazon Whispersync kommer inte att fungera, men omslagen kommer. Det är ditt val vilken funktion som är viktigare för dig. Jag uppmuntrar dig att kontakta Amazon och be dem att åtgärda detta problem.

Felet i Amazons programvara är att när du lägger en MOBI-fil på en Kindle, om inte filen är markerad som ett personligt dokument, antar Amazon att du köpte boken från dem och försöker hämta miniatyrbilden för den från sina servrar. När hämtningen misslyckas vägrar den att återgå till omslaget som definieras i MOBI-filen. Detta är sannolikt avsiktligt från Amazons sida för att försöka tvinga författare att sälja endast genom dem. Med andra ord visar Kindle bara omslag för böcker markerade som personliga dokument eller böcker som köpts direkt från Amazon.

Om du överför en MOBI-fil till en e-bläck Kindle med calibre med hjälp av en USB-anslutning, arbetar calibre runt detta Amazon-problem genom att överföra en miniatyrbild själv. Men denna lösning är bara möjligt när du använder en USB-anslutning och skickar med calibre. Observera att om du skickar via e-post, kommer Amazon automatiskt markera MOBI-filen som ett personligt dokument och omslaget kommer att fungera, men boken kommer att dyka upp i personliga dokument.

Jag överförde några böcker till min Kindle genom att använda calibre och de dök inte upp?

Böcker som skickas till Kindle visas bara upp på Kindle efter att de har blivit indexerade av Kindle. Detta kan ta lite tid. Om boken ännu inte dyker upp efter en viss tid, så är det troligt att Kindle indexering misslyckades. Ibland kan en viss bok kan orsaka indexeraren att kraschar. Tyvärr har Amazon inte lämnat något sätt att härleda vilken bok som orsakar en krasch på Kindle. Din enda utväg är att antingen återställa Kindle, eller ta bort alla filer från sitt minne med utforskaren i Windows (eller vad filhanteraren du använder) och sedan skicka böckerna till det igen, en efter en, tills du upptäcker problemboken. När du har hittat problemboken, ta bort den från Kindle och gör en MOBI till MOBI eller MOBI till AZW3 konvertering i calibre och sedan skicka tillbaka den. Detta kommer med största sannolikhet att ta hand om problemet.

Bibliotekshantering

Innehåll

- Var är bokfilerna lagrade?

- Hur hanterar calibre författarnamn och sortering?

- Varför låter calibre mig inte att lagra böcker i min egen mappstruktur?

- Varför har calibre inte en kolumn för foo?

- Kan jag ha en kolumn som visar formaten eller ISBN?

- Hur flyttar jag min calibre-data från en dator till en annan?

- Listan över böcker i calibre är tom!

- Jag får felmeddelanden med mitt calibre-biblioteket på en nätverksenhet/NAS?

Var är bokfilerna lagrade?

När du först kör calibre kommer det att be dig om en mapp där du kan lagra dina böcker. När du lägger till en bok till calibre, kommer det att kopiera boken till den mappen. Böckerna i mappen är snyggt ordnade i undermappar efter författare och titel. Observera att innehållet i den här mappen hanteras automatiskt av calibre, lägg inte till några filer/mappar manuellt i den här mappen, eftersom de kan tas bort automatiskt. Om du vill lägga till en fil kopplad till en viss bok, använd det övre högra området i dialogrutan Redigera metadata för att göra det. Sedan kommer calibre automatiskt att lägga filen i rätt mapp och flytta runt den när titeln/författaren ändras.

Metadata om böckerna lagras i filen metadata.db på den översta nivån i biblioteksmappen. Den här filen är en SQLite-databas. När du säkerhetskopierar ditt bibliotek att du kopierar hela mappen och alla dess undermappar.

Biblioteksmappen och allt dess innehåll utgör vad som kallas ett calibre-bibliotek. Du kan ha flera sådana bibliotek. För att hantera biblioteken klickar du på calibre-ikonen i verktygsfältet. Du kan skapa nya bibliotek, ta bort/byta namn på befintliga och enkelt växla mellan bibliotek.

Du kan kopiera eller flytta böcker mellan olika bibliotek (när du har mer än ett bibliotek inställning) genom att högerklicka på en bok och välj åtgärden Kopiera till bibliotek.

Hur hanterar calibre författarnamn och sortering?

Författarnamn är komplexa, särskilt över kulturer, se denna anmärkning [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en] för en del av komplexiteten. calibre har en mycket flexibel strategi för hantering av författarnamn. Det första man måste förstå är att böcker och författare är separata enheter i calibre. En bok kan ha mer än en författare och en författare kan ha mer än en bok. Du kan hantera författare till en bok genom att redigera metadatadialogrutan. Du kan hantera enskilda författare genom att högerklicka på författaren i taggbläddraren till vänster i huvud calibre-huvudfönstret och välja Hantera författare. Med denna dialogruta kan du ändra namnet på en författare och även hur namnet sorteras. Detta ändrar automatiskt författarens namn i alla författarens böcker. När en bok har flera författare, separera deras namn med &-tecknet.

Nu kommer vi till författarnamnssortering:

- När en ny författare läggs i calibre (det händer varje gång en bok av en ny författare läggs till), beräknar calibre automatiskt en sorts sträng för både boken och författaren.

- Författare i taggbläddraren sorteras efter sorteringsvärdet för authors. Kom ihåg att detta skiljer sig från författarsorteringsfältet för en bok.

- Som standard antar denna sorteringsalgoritm att författarnamnet är i formatet Förnamn Efternamn och skapar ett Efternamn, Förnamn-sorteringsvärde.

- Du kan ändra denna algoritm genom att gå till Inställningar → Avancerat → Justeringar och ställa in author_sort_copy_method justering.

- Du kan tvinga calibre att räkna om författarsorteringsvärdena för varje författare genom att högerklicka på en författare och välja Hantera författare och sedan trycka på knappen Räkna om alla författarsorteringsvärden. Gör detta efter att du har ställt in author_sort_copy_method att justera det du vill.

- Du kan tvinga calibre att räkna om författarsorteringsvärdena för alla böcker genom att använda dialogrutan för redigering av metadata i grupp (välj alla böcker och klicka på redigera metadata, markera kryssrutan Ställ automatiskt in författarsortering och tryck sedan på OK).

- Vid omräkning av författarsorteringsvärden för böcker använder calibre författarsorteringsvärden för varje enskild författare. Se därför till att de enskilda författarsorteringsvärdena är korrekta innan omräkning av böckernas författarsorteringsvärden.

- Du kan anpassa om taggbläddraren ska visa författare genom att använda deras namn eller deras sorteringsvärden genom att ställa in categories_use_field_for_author_name justering i Inställningar → Avancerat → Justeringar

Observera att du kan ställa in en enskild författares sorteringsvärde till vad du vill genom att använda Hantera Författare. Detta är användbart när det handlar om namn som calibre inte kommer att få rätt, som komplexa flerdelade namn som Miguel de Cervantes Saavedra eller när den behandlar asiatiska namn som Sun Tzu.

Med all denna flexibilitet, är det möjligt för calibre att hantera dina författarnamn som du vill. Till exempel en gemensam begäran om att få calibre att visa författarnamn EN, FN. För att göra detta, och om anmärkningen nedan inte gäller dig, då:

- Ändra author_sort_copy_method justering till copy såsom beskrivits ovan.

- Starta om calibre. Ändra inga bokmetadata innan du gör de återstående stegen.

- Ändra alla författarnamn till EN, FN genom att använda dialogrutan Hantera författare.

- När du har ändrat alla författare, tryck på knappen Räkna om alla författarsorteringsvärden.

- Tryck på OK, då kommer calibre att ändra författarna i alla dina böcker. Detta kan ta ett tag.

Observera

När man byter från FN EN till EN, FN, är det ofta så att värdena i author_sort redan är i EN, FN-format. Om så är ditt fall gör du följande:

- Ändra author_sort_copy_method justering till copy såsom beskrivits ovan.

- Starta om calibre. Ändra inga bokmetadata innan du gör de återstående stegen.

- Öppna dialogrutan Hantera författare. Tryck på knappen Kopiera alla författarsorteringsvärden till författare.

- Kontrollera författarna för att vara säker på att du är nöjd. Du kan fortfarande trycka på Avbryt för att överge ändringarna. När du trycker på OK, det finns inget att ångra.

- Tryck på OK, då kommer calibre att ändra författarna i alla dina böcker. Detta kan ta ett tag.

Varför låter calibre mig inte att lagra böcker i min egen mappstruktur?

Hela poängen med calibres bibliotekshanteringsfunktioner är att de tillhandahåller ett sök- och sorteringsbaserat gränssnitt för att hitta böcker som är mycket effektivare än något möjligt mappschema du kan komma på för din samling. När du väl blir bekväm med att använda calibres gränssnitt för att hitta, sortera och bläddra i din samling kommer du aldrig att behöva leta igenom filerna på din disk för att hitta en bok igen. Genom att hantera böcker i sin egen mappstruktur av Författare -> Titel -> Bokfiler kan calibre uppnå en hög nivå av tillförlitlighet och standardisering. För att illustrera varför ett sök-/taggbaserat gränssnitt är överlägset mappar, överväg följande. Anta att din boksamling är snyggt sorterad i mappar med följande schema:

Genre -> Author -> Series -> ReadStatus

Nu gör det mycket enkelt att hitta till exempel alla science fiction böcker av Isaac Asimov i stiftelsen serien. Men antar att du vill hitta alla olästa science fiction böcker. Det finns inget enkelt sätt att göra detta med det här mappsystemet, du måste istället ha ett mappschema som ser ut som:

ReadStatus -> Genre -> Author -> Series

I calibre skulle du istället använda taggar för att markera genre och lässtatus och sedan bara använda en enkel sökförfråga som tag:scifi and not tag:läst. calibre har till och med ett snyggt grafiskt gränssnitt, så du behöver inte lära dig dess sökspråk, istället kan du bara klicka på taggar för att inkludera eller exkludera dem från sökningen.

Till er som hävdar att du behöver tillgång till filsystemet, på så att du kan få tillgång till dina böcker över nätet, har calibre en utmärkt innehållsserver som ger dig tillgång till ditt calibre-biblioteket över nätet.

Om du är orolig att en dag calibre kommer att upphöra att utvecklas och att lämna alla dina böcker strandsatta i sin mappstruktur, utforska den kraftfulla Spara till disk-funktionen i calibre som låter dig exportera alla dina filer i en mappstruktur för godtycklig komplexitet baserat på sina metadata.

Slutligen är anledningen till att det finns siffror i slutet av varje titelmapp är för robusthet. Det numret är bokpostens ID-nummer i calibre-databasen. Med närvaron av numret kan du ha flera poster med samma titel och författarnamn. Det är också en del av det som tillåter calibre att magiskt återgenerera databasen med alla metadata om databasfilen blir korrupt. Med tanke på att calibres uppdrag är att få dig att sluta lagra metadata i filnamn och sluta använda filsystemet för att hitta saker, är den ökade robustheten som ges av id-nummer väl värt de fulare mappnamnen.

Om du fortfarande inte är övertygad är jag rädd att calibre inte är något för dig. Leta någon annanstans för dina bokkatalogiseringsbehov. Bara för att vara tydlig, det här kommer inte att förändras. Kontakta oss inte i ett försök att få oss att ändra på detta.

Varför har calibre inte en kolumn för foo?

calibre är designad för att ha kolumner för de mest frekventa och mest använda områdena. Dessutom kan du lägga till vilka kolumner du vill. Kolumner kan läggas till via Inställningar->Gränssnitt->Lägg till dina egna kolumner. Titta på handledningen Gränsnittets potentialtips [https://calibre-ebook.com/sv/demo#tutorials] för att lära dig hur du skapar dina egna kolumner, eller läs det här blogginlägget [http://blog.calibre-ebook.com/2011/11/calibre-custom-columns.html].

Du kan också skapa ”virtuella kolumner” som innehåller kombinationer av metadata från andra kolumner. I dialogrutan Lägg till kolumnen använder du Snabbskapa-länkarna för att enkelt skapa kolumner för att visa bokens ISBN eller format. Du kan använda det kraftfulla mallspråket för calibre för att göra mycket mer med kolumner. För mer information, se Mallspråket för calibre.

Kan jag ha en kolumn som visar formaten eller ISBN?

Ja, det kan du. Följ instruktionerna i svaret ovan för att lägga till anpassade kolumner.

Hur flyttar jag min calibre-data från en dator till en annan?

Du kan exportera alla calibre-data (böcker, inställningar och insticksmoduler) och sedan importera den till en annan dator. Låt oss först se hur du exporterar data:

- Högerklicka på calibre ikonen i calibre huvudverktygsfältet och välj Exportera/importera alla calibre-data. Observera att om det för närvarande finns en enhet ansluten, kommer det här menyalternativet inte att vara tillgängligt – så koppla loss alla anslutna enheter. Klicka sedan på knappen märkt Exporterar alla calibre-data. Du kommer att se en lista med alla dina calibre-bibliotek. Klicka på OK och välj en tom mapp någonstans på din dator. Exporterade data sparas i den här mappen. Kopiera helt enkelt den här mappen till den nya datorn och följ instruktionerna nedan för att importera data.

- Installera calibre på din nya dator och gå igenom välkomstguiden, det spelar ingen roll vad du gör där, eftersom du kommer att importera dina gamla inställningar i nästa steg. Du kommer nu ha ett tomt calibre med bara guiden Komma igång i ditt bibliotek. Högerklicka igen på calibre-knappen och välj Exportera/importera alla calibre-data. Klicka sedan på knappen märkt Importera tidigare exporterade data. Välj mappen med den exporterade data som du kopierade över tidigare. Du får nu en lista över bibliotek som du kan importera. Gå igenom listan en efter en och välj den nya platsen för varje bibliotek (en plats är bara en tom mapp någonstans på din dator). Klicka på OK. När importen är klar kommer calibre att starta om, med alla dina gamla bibliotek, inställningar och calibre-insticksmoduler.

Observera

Denna import/export funktion är endast tillgänglig från calibre version 2.47 och framåt. Om du har en äldre version av calibre, eller om du stöter på problem med import/export, kan du bara kopiera över din calibre-biblioteksmapp manuellt, som beskrivs i nästa stycke.

Kopiera helt enkelt calibre-biblioteksmappen från den gamla till den nya datorn. Du kan ta reda på vilken biblioteksmappen är genom att klicka på calibre-ikonen i verktygsfältet. Välj åtgärden Växla/skapa calibre-bibliotek och du kommer se sökvägen till det aktuella calibre-biblioteket.

Nu på den nya datorn, starta calibre för första gången. Det kommer att köra välkomstguiden som ber dig om calibre-bibliotekets placering. Hänvisa den till den tidigare kopierade mappen. Om datorn som du överför till redan har en calibre-installation körs inte välkomstguiden. I så fall högerklickar du på calibre-ikonen i verktygsfältet och hänvisar den till den nyligen kopierade mappen. Du kommer nu ha två calibre-bibliotek på din dator och du kan växla mellan dem genom att klicka på calibre-ikonen i verktygsfältet. Överföring av ditt bibliotek på detta sätt bevarar alla dina metadata, taggar, anpassade kolumner osv.

Listan över böcker i calibre är tom!

För att förstå varför det hände, måste du förstå vad calibre-biblioteket är. På den mest grundläggande nivån, är ett calibre-bibliotek bara är en mapp. När du lägger till en bok till calibre, är att bokens filer kopieras till denna mapp (ordnade i undermappar efter författare och titel). Inne i calibre-bibliotekmappen, på den högsta nivån, kommer du att se en fil som heter metadata.db. Den här filen är där calibre lagrar metadata som titel/författare/betyg/taggar osv. för varje bok i ditt calibre-bibliotek. Listan med böcker som calibre visar skapas genom att läsa innehållet i den här metadata.db-filen.

Det kan finnas två skäl till att calibre visar en tom lista med böcker:

- Din biblioteksmapp i calibre ändrade sin plats. Det här kan hända om det var på en extern hårddisk och enhetsbokstaven för disken ändrades. Eller om du av misstag flyttade mappen. I det här fallet kan calibre inte hitta sitt bibliotek och startar med ett tomt bibliotek istället. För att råda bot på detta, högerklicka på calibre-ikonen i calibre-verktygsfältet och välj Växla/skapa bibliotek. Klicka på den lilla blå ikonen för att välja den nya platsen för ditt calibre-bibliotek och klicka på OK. Om du inte känner till den nya platsen, sök efter filen metadata.db på din dator.

- Din metadata.db-fil togs bort/skadades. I så fall kan du be calibre att bygga om metadata.db från sina säkerhetskopior. Högerklicka på ikonen calibre i calibre-verktygsfältet och välj Biblioteksunderhålls->Återställ databas. calibre kommer automatiskt återuppbygga metadata.db.

Jag får felmeddelanden med mitt calibre-biblioteket på en nätverksenhet/NAS?

Lägg inte ditt calibre-bibliotek på en nätverksenhet.

Ett filsystem är ett komplext odjur. De flesta nätverksfilsystem saknar olika filsystemfunktioner som calibre använder. Vissa stöder inte fillåsning, andra stöder inte hårdlänkning, andra är bara opålitliga. Dessutom är calibre ett program för en enda användare. Om du av misstag kör två kopior av calibre på samma nätverksbibliotek, kommer dåliga saker att hända. Slutligen inför olika operativsystem olika begränsningar för filsystem, så om du delar din nätverksenhet över flera operativsystem, kommer återigen dåliga saker att hända.

Överväg att använda calibre-innehållsservern för att göra dina böcker tillgängliga på andra datorer. Kör calibre på en enda dator och få åtkomst till den via innehållsservern eller en fjärrskrivbordslösning.

Om du måste dela det faktiska biblioteket, använd ett filsynkroniseringsverktyg som DropBox eller rsync istället för en nätverksenhet. Om du använder ett filsynkroniseringsverktyg är det viktigt att du ser till att både calibre och filsynkroniseringsverktyget inte försöker komma åt calibre-biblioteket samtidigt. Med andra ord, kör inte filsynkroniseringsverktyget och calibre samtidigt.

Även med dessa verktyg finns risk för datakorruption/förlust, så gör det bara om du är villig att leva med den risken. Var särskilt medveten om att Google Drive är oförenligt med calibre. Om du placerar ditt calibre-bibliotek i Google Drive kommer du att drabbas av dataförlust. Se denna tråd [https://www.mobileread.com/forums/showthread.php?t=205581] för detaljer.

Diverse

Innehåll

- Amazon stoppar e-postleverans av MOBI-filer?

- Jag vill att calibre hämtar nyheter från min favorit nyhetswebbplats.

- Varför namnet calibre?

- Varför visar calibre bara några av mina teckensnitt på macOS?

- calibre startar inte på Windows?

- calibre fryser/kraschar ibland?

- calibres e-bokvisare och redigeringsverktyget fungerar inte på Windows?

- Användning av visaren eller att göra några konverteringar resulterar i ett felmeddelande om tillstånd nekad på Windows

- calibre startar inte/kraschar på macOS?

- Jag får bara en svart eller vit skärm när jag kör calibres e-bokvisare?

- Jag hämtade installationsprogrammet, men det fungerar inte?

- Mitt antivirusprogram påstår att calibre är ett virus/trojan?

- Hur säkerhetskopierar jag calibre?

- Hur använder jag köpta EPUB-böcker med calibre (eller vad ska jag göra med .acsm-filer)?

- Jag får felmeddelandet ”Åtkomst nekad”?

- Kan jag få kommentarmetadata att dyka upp på min läsenhet?

- Hur får jag calibre att använda min HTTP-proxy?

- Jag vill ha en del funktioner tillagda i calibre. Vad kan jag göra?

- Varför har inte calibre en automatisk uppdatering?

- Hur är calibre licensierad?

- Hur kör jag calibre från mitt USB-minne?

- Hur kör jag delar av calibre som nyhetshämtning och innehållsserver på min egen Linux-server?

Amazon stoppar e-postleverans av MOBI-filer?

Amazon har meddelat [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/] att de kommer att sluta acceptera MOBI-filer som skickas till @kindle.com e-postadresser. Du kan instruera calibre att skicka EPUB istället för MOBI genom att gå till Inställningar → Dela böcker via e-post och sedan ta bort MOBI från listan över format att skicka till din @kindle.com e-postadress och lägga till EPUB istället.

Observera dock att Amazons EPUB-inmatning är mycket felaktigt, de kommer att avvisa ett antal EPUB-filer som fungerar överallt annars. I sådana fall kan du prova följande knep:

1. Konvertera EPUB-filen till MOBI

2. Konvertera sedan MOBI-filen tillbaka till EPUB och skicka den resulterande EPUB-filen

Detta kommer att ta bort all avancerad formatering, inbäddade teckensnitt, osv., men ökar avsevärt chanserna att Amazon accepterar EPUB-filen.

Observera

Om du tidigare använde e-postleverans av tidskrifter som hämtats med calibre, är det bättre för dig att skicka dem med USB-kabel eller hämta dem från calibre-innehållsservern via Kindles inbyggda webbläsare. Men om du vill fortsätta att använda e-postleverans kan du prova att ändra utmatningsformatet i Inställningar->Beteende till EPUB, då hämtar calibre nyheterna i EPUB-format. Huruvida Amazon kommer att acceptera EPUB-filen eller inte är en helt annan fråga.

Jag vill att calibre hämtar nyheter från min favorit nyhetswebbplats.

Om du är någorlunda skicklig med datorer, kan du lära calibre att hämtar nyheter från en webbplats som du väljer. Om du vill veta hur du gör detta se Lägga till din favorit nyhetswebbplats.

Annars kan du begära en viss nyhetssajt genom att posta i calibre-receptforumet [https://www.mobileread.com/forums/forumdisplay.php?f=228].

Varför namnet calibre?

Gör ditt val:

- Converter And LIBRary for E-books

- En produkt av hög kaliber (calibre)

- En hyllning till SONY Librie som var den första e-bläckbaserade e-bokläsenheten

- Min fru valde det ;-)

calibre uttalas som cal-i-ber inte ca-li-bre. Om du undrar är calibre den brittiska/samväldets stavning för caliber. För att vara indisk, är det den naturliga stavningen för mig.

Varför visar calibre bara några av mina teckensnitt på macOS?

calibre bäddar in teckensnitt i e-bokfiler som skapas. E-bokfiler stöder endast inbäddning av Truetype och Opentype (.ttf och .otf) teckensnitt. De flesta teckensnitt på macOS-system är i .dfont-format, vilka inte kan bäddas in. calibre visar endast Truetype- och Opentype-teckensnitt som finns på ditt system. Du kan få många sådana teckensnitt på webben. Helt enkelt hämtar .ttf-/.otf-filer och lägga till dem i biblioteks-/teckensnittsmappen i din hemmapp.

calibre startar inte på Windows?

Det kan finnas flera orsaker till det här:

- Om du inte får några felmeddelanden men calibre-fönstret inte visas har det förmodligen precis dykt upp utanför skärmen. Du kan samla alla fönster på den aktuella skärmen med en av teknikerna som beskrivs här [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows].

- Om du får ett felmeddelande om att calibre inte kan öppna en fil eftersom den används av ett annat program, gör följande:

- Avinstallera calibre

- Starta om din dator

- Återinstallera calibre. Men starta inte calibre från installationsguiden.

- Inaktivera tillfälligt ditt antivirusprogram (koppla bort från internet innan du gör det, för att vara säker)

- Titta inne i mappen du valt till ditt calibre-bibliotek. Om du ser en fil vid namn metadata.db, ta bort den.

- Starta calibre

- Från och med nu bör du kunna starta calibre normalt.

- Om du får ett fel i en Python-funktion som avslutas oväntat efter en uppgradering av calibre, först avinstallera calibre, sedan ta bort mappar (om det finns några) C:\Program Files\Calibre och C:\Program Files\Calibre2. Nu återinstallera och du bör vara OK.

- Om du får ett fel i välkomstguiden vid en initieringskörning av calibre, försök välja en mapp som C:\library för calibre-biblioteket (eftersom calibre ibland har problem med biblioteksplatser om sökvägen innehåller icke-engelska tecken, eller bara siffror osv.)

- Försök köra som administratör (högerklicka på ikonen och välj Kör som administratör)

Om det fortfarande inte startar, starta en kommandotolk (tryck på Windows-tangenten och R; skriv sedan cmd.exe i dialogrutan Kör som visas). Skriv följande kommando i kommandotolken och tryck på enter:

calibre-debug -g

Lägg upp någon utmatning som du ser i ett hjälpmeddelande på forumet [https://www.mobileread.com/forums/forumdisplay.php?f=166].

calibre fryser/kraschar ibland?

Det finns flera möjliga saker som jag känner till som kan orsaka detta:

- Du har nyligen anslutit en extern bildskärm eller TV till din dator. I detta fall, när calibre öppnar ett nytt fönster som redigeringsmetadatafönstret eller konverteringsdialogrutan, visas det på den andra bildskärmen där du inte märker det och så du tror att calibre har frysts. Koppla bort din andra bildskärm och starta om calibre.

- Följande program har rapporterats orsaka krascher i calibre: Om du kör någon av dessa, stäng av dem innan calibre, eller avinstallera dem: RoboForm, Logitech SetPoint Settings, Constant Guard Protection by Xfinity, Spybot, Killer Network Manager, Nahimic UI Interface, Acronis True Image.

- Du använder en USB-mus/surfplatta av märket Wacom. Det finns en inkompatibilitet mellan Wacom-drivrutiner och grafikverktygssatserna som calibre använder. Försök använda en mus som inte är av märket Wacom.

- På vissa 64-bitarsversioner av Windows finns säkerhetsmjukvara/inställningar som förhindrar att 64-bitars calibre fungerar korrekt. Om du använder 64-bitarsversionen av calibre, försök att byta till 32-bitarsversionen.

- Om kraschen uppstår när du försöker kopiera text från calibres e-bokvisare, är det sannolikt att det orsakas av vissa urklipp övervaknings-/hanteringsprogram som du har igång. Stäng av dem och du bör vara ok.

- Om kraschar inträffar specifikt när du använder en fildialog, som att klicka på Lägg till böcker- eller Spara till disk-knappen, har du programvara som har installerat trasiga skaltillägg på din dator. Kända synder inkluderar: SpiderOak, odrive sync och Dell Backup and Recovery and NetDrive. Om du har en av dessa, avinstallera dem och det kommer att fungera. Du kan också använda NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html] för att se vad skal förlängningar är installerade på datorn och stänga av dem individuellt, om du inte vill avinstallera hela programmet. Kom ihåg att använda ”Starta om utforskaren” eller starta om datorn efter att du inaktiverat skaltilläggen.

Om inget av ovanstående gäller dig, så finns det något annat program på din dator som stör calibre. Starta först om din dator i felsäkert läge, för att köra så få program som möjligt och se om krascherna fortfarande inträffar. Om de inte gör det vet du att det är något program som orsakar problemet. Den troligaste syndabocken är ett program som ändrar andra programs beteende, till exempel ett antivirusprogram, en enhetsdrivrutin, något som RoboForm (en automatisk formulärifyllningsapp) eller en hjälpteknik som röstkontroll eller en skärmläsare.

Det enda sättet att hitta den skyldige är att eliminera programmen ett efter ett och se vilket som orsakar problemet. I princip, stoppa ett program, kör calibre, kontrollera om det kraschar. Om de fortfarande inträffar, stoppa ett annat program och upprepa.

calibres e-bokvisare och redigeringsverktyget fungerar inte på Windows?

Dessa två program använder hårdvaruacceleration när de bäddar in en version av webbläsaren Chrome för att rendera HTML. Om de inte fungerar kommer det att bero på inkompatibilitet med ditt systems GPU (grafik) drivrutiner. Försök att uppdatera dessa först och starta om. Om det inte löser det kan du ställa in miljövariabeln QTWEBENGINE_CHROMIUM_FLAGS till värdet --disable-gpu för att stänga av hårdvaruacceleration. Se den här sidan [https://doc.qt.io/qt-6/qtwebengine-debugging.html] för detaljer.

Användning av visaren eller att göra några konverteringar resulterar i ett felmeddelande om tillstånd nekad på Windows

Något på datorn hindrar calibre från att komma åt sina egna temporära filer. Troligtvis är behörigheterna på din Temp-mapp felaktiga. Gå till mappen file:C:\Användare\ANVÄNDARNAMN\AppData\Local i Windows utforskaren och högerklicka på mappen file:` Temp`, välj Egenskaper och gå till fliken Säkerhet. Se till att ditt användarkonto har full kontroll på den här mappen.

Vissa användare har rapporterat att körning av följande kommando i en administratörskommandotolk löser deras behörigheter. För att få en administratörskommandotolk sök efter cmd.exe i startmenyn, högerklicka på kommandotolksposten och välj Kör som administratör. Skriv följande kommando i kommandotolken och tryck på enter:

icacls "%appdata%\..\Local\Temp" /reset /T

Alternativt kan du köra calibre som administratör, men detta kommer att leda till vissa problem, till exempel att dra och släppa inte fungerar.

Slutligen har en del användare rapporterat att inaktivera UAC löser problemet.

calibre startar inte/kraschar på macOS?

En vanlig orsak till fel på macOS är användningen av tillgänglighetsteknik som är oförenliga med grafikverktygen calibre använder. Försök med att stänga av VoiceOver om du har det på. Också gå till System Inställningar->System->Hjälpmedel och stäng av inställningen för att ge tillgång till hjälpmedel i alla flikar. En annan orsak kan vara en tredjepartsapps som modifierar systembeteende, såsom Smart Scroll.

Du kan få felsökningsutmatning om varför calibre inte startar genom att köra Console.app. Felsökningsutmatning kommer att skrivas till den. Om felsökningsutmatningen innehåller en rad som ser ut så här:

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

då är problemet troligen en skadad teckensnittscache. Du kan rensa cachen genom att följa dessa instruktioner [https://www.macworld.com/article/1139383/fontcacheclear.html]. Om det inte löser det, leta efter en skadad teckensnittsfil på ditt system, i ~/Library/Fonts eller liknande. Ett enkelt sätt att kontrollera om det finns skadade teckensnitt i macOS är att starta programmet ”Font Book”, välj alla teckensnitt och sedan i Arkiv-menyn, välj ”Verifiera teckensnitt”.

Jag får bara en svart eller vit skärm när jag kör calibres e-bokvisare?

Detta kommer att bero på en inkompatibilitet mellan Qt WebEngine, som visaren använder för att ta bort och GPU-drivrutinerna på ditt system. Försök först att uppgradera GPU-drivrutinerna. Om det inte hjälper kan du försöka stänga av hårdvaruacceleration i Qt WebEngine genom att ställa in miljövariabeln QTWEBENGINE_CHROMIUM_FLAGS till värdet --disable-gpu. Se Miljövariabler för hur man ändrar miljövariabler.

Jag hämtade installationsprogrammet, men det fungerar inte?

Hämtning från internet kan ibland resultera i en skadad hämtning. Om calibre-installationsprogrammet som du hämtade inte startar kan du testa att hämtar det igen. Om du hämtar det igen och det ändå inte fungerar kan du hämta det från en alternativ plats [https://github.com/kovidgoyal/calibre/releases/latest]. Om installationsprogrammet fortfarande inte fungerar förhindrar något på din dator att det körs.

- Försök att tillfälligt inaktivera antivirusprogrammet (Microsoft Security Essentials eller Kaspersky eller Norton eller McAfee eller vad). Detta är troligen den skyldige om uppgraderingen hänger i mitten.

- På samma sätt, om installationsprogrammet misslyckas/återställs och du har Microsoft PowerToys igång, avsluta det.

- Försök att starta om din dator och kör ett registerrensningsprogram som Wise Registry Cleaner [https://www.wisecleaner.com].

- Försök med en ren installation. Det vill säga, avinstallera calibre, ta bort: C:\Program Files\Calibre2 (eller var du tidigare valde att installera calibre). Därefter ominstallera calibre. Observera att avinstalleringen inte rör dina böcker eller inställningar.

- Testa att hämta installationsprogrammet med en alternativ webbläsare. Om du till exempel använder Microsoft Edge, testa istället använda Firefox eller Chrome.

- Om du får ett felmeddelande om en saknad DLL på Windows, är behörigheterna för din tillfälliga mapp troligen felaktiga. Gå till mappen C:\Users\USERNAME\AppData\Local i Windows utforskaren och högerklicka sedan på mappen Temp och välj Egenskaper och gå till fliken Säkerhet. Se till att ditt användarkonto har fullständig kontroll över den här mappen.

Om du fortfarande inte kan få installationsprogrammet att fungera och du använder Windows, kan du använda calibre portable-installation [https://calibre-ebook.com/sv/download_portable], som inte behöver ett installationsprogram (det är bara en ZIP-fil).

Mitt antivirusprogram påstår att calibre är ett virus/trojan?

Det första du bör kontrollera är att du hämtar calibre från den officiella webbplatsen [https://calibre-ebook.com/sv/download]. Se till att du klicka på hämtningslänkar till vänster, inte på annonserna till höger. calibre är ett mycket populärt program och skrupelfria personer försöker skapa webbplatser som erbjuder det för hämtning för att lura oförsiktiga.

Om du har den officiella hämtningen och ditt antivirusprogram fortfarande hävdar att calibre är ett virus, är alltså felet dit antivirusprogram. Antivirusprogram använder heuristik, kodmönster som ”ser misstänkta” ut för att upptäcka virus. Det är snarare som rasprofilering. calibre är en helt öppen källkodsprodukt. Du kan faktiskt bläddra i källkoden själv (eller anlita någon att göra det åt dig) för att verifiera att det inte är ett virus. Rapportera falsk identifikation till företaget du köper ditt antivirusprogram från. Om antivirusprogrammet hindrar dig från att hämta/installera calibre, inaktivera det tillfälligt, installera calibre och aktivera det sedan igen.

Hur säkerhetskopierar jag calibre?

Det viktigaste för att säkerhetskopiera är calibre-biblioteksmappen som innehåller alla dina böcker och metadata. Detta är den mapp du valde för ditt calibre-bibliotek när du körde calibre för första gången. Du kan få sökvägen till biblioteksmappen genom att klicka på calibre-ikonen på verktygslisten. Du måste säkerhetskopiera den här hela mappen med alla filer och undermappar.

Du kan växla calibre till att använda en säkerhetskopierad biblioteksmapp genom att klicka på calibre-ikonen i verktygsfältet och välja din säkerhetskopierade biblioteksmapp. En säkerhetskopierad biblioteksmappen säkerhetskopierar dina egna kolumner och sparade sökningar samt alla dina böcker och metadata.

Om du vill säkerhetskopiera calibre-konfigurationen/insticksmoduler måste du säkerhetskopiera konfigurationsmappen. Du hittar den här konfigurationsmappen via Inställningar → Diverse. Observera att återställning av konfigurationsmappar inte stöds officiellt, men bör fungera i de flesta fall. Kopiera bara innehållet i säkerhetskopieringsmappen till den aktuella konfigurationsmappen för att återställa.

Hur använder jag köpta EPUB-böcker med calibre (eller vad ska jag göra med .acsm-filer)?

De flesta köpta EPUB-böcker har DRM. Det här förhindrar calibre från att öppna dem. Du kan fortfarande använda calibre för att lagra och överföra dem till din e-bokläsenhet. Först måste du auktorisera din läsenhet på en Windows-maskin med Adobe Digital Editions. När detta är gjort kommer EPUB-böcker som överförts med calibre att fungera bra på din läsenhet. När du köper en EPUB-bok från en webbplats kommer du att få en ”.acsm”-fil. När den här filen öppnas med Adobe Digital Editions kommer det att hämta den faktiska ”.epub”-e-boken. E-bokfilen sparas i mappen ”My Digital Editions”, varifrån du lägga till den i calibre.

Jag får felmeddelandet ”Åtkomst nekad”?

En fel om nekad åtkomst kan uppstå på grund av många möjliga orsaker, ingen av dem har något att göra med calibre.

- Du kan få felmeddelanden om nekad åtkomst om du använder ett SD-kort med skrivskydd aktiverat.

- På macOS om du får behörighetsfel när du ansluter en enhet till calibre kan du åtgärda det genom att titta under Systeminställningar > Säkerhet och sekretess > Sekretess > Filer och mappar.

- Om du, eller något program du använde, ändrade filbehörigheterna för filerna i fråga till skrivskyddade.

- Om det finns ett filsystemfel på enheten som fick ditt operativsystem att montera filsystemet i skrivskyddat läge eller markera en viss fil som skrivskyddad i väntan på återställning.

- Om filerna har deras ägare inställd till någon annan användare än du.

- Om din fil är öppen i ett annat program.

- Om filen finns på en enhet kan du ha nått gränsen för högst 256 filer i roten av enheten. I det här fallet måste du formatera enheten/sd-kortet som hänvisas till felmeddelandet med ett FAT32-filsystem, eller ta bort några filer från SD-kortet/enhetsminnet.

Du måste åtgärda den underliggande orsaken till behörighetsfelet innan du fortsätter att använda calibre. Läs felmeddelandet noggrant, se vilken fil den hänvisar till och åtgärda behörigheterna för den filen eller dess mappar.

Kan jag få kommentarmetadata att dyka upp på min läsenhet?

De flesta läsenheter stöder inte detta. Du bör klaga hos tillverkaren om det och förhoppningsvis kommer det att förändras om tillräckligt många klagar. Under tiden kan du infoga metadata, inklusive kommentarer på en ”Omslagssida” i början av e-boken, genom att använda alternativet ”Infoga metadata som en sida i början på boken” under konverteringen. Alternativet finns i avsnittet Strukturdetektering i konverteringsinställningarna. Observera att för att detta ska ha effekt måste du konvertera boken. Om din bok är redan i ett format som inte behöver konverteras, kan du konvertera från det formatet till samma format.

Ett annat alternativ är att skapa en katalog i e-bokform som innehåller en lista över alla böcker i ditt calibre-bibliotek, med deras metadata. Klicka och håll knappen Konvertera för att komma åt verktyget för att skapa katalog. Och innan du frågar, nej du kan inte ha katalogen att ”länka direkt till” böcker på din läsenhet.

Hur får jag calibre att använda min HTTP-proxy?

Som standard använder calibre de tillgängliga proxyinställningar som är inställda i ditt operativsystem. Ibland är dessa felaktiga, till exempel på Windows, om du inte använder Microsoft Edge, kanske proxyinställningar inte är uppdaterade. Du kan tala om för calibre att använda en viss proxyserver genom att ställa in miljövariablerna http_proxy och https_proxy. Formatet på variabeln är: http://username:password@servername, du bör fråga din nätverksadministratör för att ge dig rätt värde för denna variabel. Observera att calibre endast stöder HTTP-proxyer och inte SOCKS-proxyer. Du kan se de aktuella proxyerna som används av calibre i Inställningar->Diverse.

Jag vill ha en del funktioner tillagda i calibre. Vad kan jag göra?

Du har två val:

1. Skapa en lösning genom att hacka calibre och skicka det till mig för granskning och inkludering. Se utveckling [https://calibre-ebook.com/sv/get-involved].

2. Öppna en funktionsförfrågan [https://calibre-ebook.com/sv/bugs]. Kom ihåg att även om du tror att din funktionsförfrågan är oerhört viktigt/nödvändlig, kanske inte calibre-utvecklarena håller med. Lyckligtvis har calibre öppen källkod, vilket innebär att du alltid har möjlighet att implementera din funktion själv, eller anställa någon att göra det åt dig. calibre har en omfattande insticksmodulsarkitektur, så du kanske kan utveckla din funktion som ett insticksmodul, se Skriv dina egna insticksmoduler för att utöka calibres funktionalitet.

Varför har inte calibre en automatisk uppdatering?

För många orsaker:

- Det finns ingen anledning att uppdatera varje vecka. Om du är nöjd med hur calibre fungerar så stäng av uppdateringaviseringar och vara på god väg. Återkom för att se om du vill uppdatera en gång om året eller så. Det finns en kryssruta för att stänga av uppdateringsaviseringen, på själva uppdateringsaviseringen.

- calibre-hämtningar använder för närvarande omkring 150TB bandbredd per månad [https://calibre-ebook.com/dynamic/downloads]. Att Implementera automatiska uppdateringar skulle avsevärt öka det och i slutändan kosta tusentals dollar i månaden, vilket någon måste betala.

- Om jag implementerar en dialogruta som hämtar uppdateringen och startar den, istället för att gå till webbplatsen som den gör nu, skulle det spara den ivriga calibre-uppdateraren, högst fem klick i veckan. Det finns betydligt högre prioriteringar att göra i calibre-utvecklingen.

- Om du verkligen, verkligen hatar att hämta calibre varje vecka men ändå vill vara uppdaterad, rekommenderar jag dig att köra från källkod, vilket gör uppdateringen trivial. Instruktioner är tillgängliga här.

- Det finns automatiska tredjepartsuppdaterare för calibre gjorda av calibre-användare på calibre-forumet [https://www.mobileread.com/forums/forumdisplay.php?f=238].

Hur är calibre licensierad?

calibre licensieras under GNU General Public License v3 (en öppen källkodslicens). Det innebär att du är fri att omdistributera calibre så länge du gör källkoden tillgänglig. Så om du vill lägga calibre på en CD med din produkt måste du också lägga till calibres källkod på CD:en. Källkoden är tillgänglig för hämtning [https://download.calibre-ebook.com]. Du är fri att använda resultaten av konverteringar från calibre hur du vill. Du kan inte använda endera kod eller bibliotek från calibre i ditt program utan att också öppna källkoden för din mjukvara. För mer information, se GNU GPL v3 [https://www.gnu.org/licenses/gpl.html].

Hur kör jag calibre från mitt USB-minne?

En bärbar version av calibre är tillgänglig här [https://calibre-ebook.com/sv/download_portable].

Hur kör jag delar av calibre som nyhetshämtning och innehållsserver på min egen Linux-server?

Först måste du installera calibre på din Linux-server. Om din server använder en modern Linux-distribution, borde du inte ha några problem med att installera calibre på den.

Observera
calibre behöver GLIBC >= 2.31 och libstdc++ >= 6.0.28. Om du har en äldre server behöver du antingen kompilera dessa från källkod, eller använd calibre 3.48 som kräver GLIBC >= 2.17 eller 2.85.1 som kräver GLIBC >= 2.13 eller calibre 1.48 som endast kräver GLIBC >= 2.10. Dessutom, även om kommandoradsverktygen för calibre inte behöver ha en körande X-server, kräver vissa av dem att X-serverbiblioteken är installerade på ditt system. Detta beror på Qt, som används för bildbehandlingsuppgifter, och länkar mot dessa bibliotek. Om du får ett importfel om några Qt-moduler, är det sannolikt att du saknar vissa X-bibliotek. Typiska kandidater är: libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0.

Du kan köra calibre-servern med kommandot:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

Du kan hämtar nyheter och konvertera dem till en e-bok med kommandot:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

Om du vill skapa MOBI, använd istället outputfile.mobi och använd --output-profile kindle.

Du kan skicka e-post med hämtade nyheter med kommandot:

/opt/calibre/calibre-smtp

Jag lämnar att lista ut exakt kommandorad som en övning för läsenheten.

Slutligen, du kan lägga till hämtade nyheter i calibre-biblioteket med:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

Kom ihåg att läsa Kommandoradsgränssnitt i användarmanualen för calibre för mer information om dessa och andra kommandon.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar

Handledningar

Här hittar du handledningar för att komma igång med calibres mer avancerade funktioner, t.ex. XPath och mallar.

- Lägga till din favorit nyhetswebbplats

- Hantera undergrupper av böcker, till exempel ”genre”

- XPath-handledning

- Mallspråket för calibre

- Allt om att använda reguljära uttryck i calibre

- Skriv dina egna insticksmoduler för att utöka calibres funktionalitet

- Matematiktypsättning i e-böcker
- Skapa kataloger för AZW3 • EPUB • MOBI
- Virtuella bibliotek

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »

- Hantera undergrupper av böcker, till exempel ”genre”

Hantera undergrupper av böcker, till exempel ”genre”

Vissa personer vill organisera böckerna i sitt bibliotek i undergrupper, liknande undermappar. Den vanligaste anledningen är att skapa genrehierarkier, men det finns många andra. En användare bad om ett sätt att organisera läroböcker efter ämne och kursnummer. En annan ville hålla reda på gåvor efter ämne och mottagare. Den här handledningen kommer att använda genreexemplet för resten av det här inlägget.

Innan vi går vidare, observera att vi inte talar om mappar på hårddisken. Undergrupper är inte mappar. Böcker kopieras inte någonstans. calibres bibliotek filstruktur påverkas inte. Istället presenterar vi ett sätt att organisera och visa undergrupper av böcker inom ett calibre-bibliotek.

- Anpassa

- Söka

- Begränsningar

- Användbara mallfunktioner

De vanligt förekommande kraven för undergrupper som genrer är:

- En undergrupp (t.ex. en genre) måste innehålla (hänvisa till) böcker, inte kategorier av böcker. Detta är vad som skiljer undergrupper från calibre-användarkategorier.

- En bok kan vara i flera undergrupper (genrer). Detta skiljer grupper från fysiska filmappar.

- Undergrupper (genrer) måste bilda en hierarki; undergrupper kan innehålla undergrupper.

Taggar ger dig de två första. Om du taggar en bok med genren kan du använda taggbläddraren (eller sök) för att hitta böckerna med den genren, vilket ger dig den första. Många böcker kan ha samma tagg eller taggar, vilket ger dig den andra. Problemet är att taggar inte uppfyller det tredje kravet. De ger ingen hierarki.

calibre-hierarkifunktionen ger dig den tredje – förmågan att se genrerna i ett ”träd” och förmågan att enkelt söka efter böcker i genre eller undergenre. Antag till exempel att din generstruktur liknar följande:

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

Genom att använda hierarkifunktionen kan du se dessa genrer i taggbläddraren i trädform, som visas på skärmbilden. I detta exempel den yttersta nivån (Genre) är en anpassad kolumn som innehåller genrer. Genrer som innehåller undergenrer visas med en liten triangel bredvid dem. Genom att klicka på den triangel ska öppna posten och visa undergenrer, som ni kan se med historia och science-fiction.

Genom att klicka på en genre kan sökning ske efter alla böcker med denna genre eller underkategorier i samma genre. Till exempel kan klicka på science fiction ge alla tre av de underordnade genrer, alternativ historia, militär och rymdopera. Genom att klicka på alternativ historia kommer att ge böcker i denna genre, och ignorera de i militär and rymdopera. Naturligtvis kan en bok ha flera genres. Om en bok har både rymdopera -och militärgenrer, så kommer du se att boken om du klickar på någon av genrerna. Sökning diskuteras mer i detalj nedan.

En annan sak du kan se från bilden är att genren militär visas två gånger, en gång under historia och en gång under science fiction. Eftersom genrerna är i en hierarki är dessa två skilda genrer. En bok kan vara i den ena, i den andra eller (tveklöst i det här fallet) båda. Till exempel kan böckerna i Winston Churchills ”Andra världskriget” vara i ”historia.militär”. David Webers Honor Harrington böckerna kan vara i ”science fiction.militär”, och för den delen även i ”science fiction.rymdopera.”

När en genre finns, det är att genren har minst en bok, kan du enkelt tillämpa den på andra böcker genom att dra böcker från biblioteket vyn till genren som du vill att böckerna ska ha. Du kan också använda genrer i metadata redaktörer; mer om detta nedan.

Anpassa

Vid det här laget, kanske din fråga vara ”Hur var allt detta upplagt?” Det finns tre steg: 1) skapade den anpassade kolumnen, 2) berättade för calibre att den nya kolumnen ska betraktas som en hierarki, och 3) lade till genrer.

Du skapar den anpassade kolumnen på vanligt sätt, med hjälp av Inställningar -> Lägg till dina egna kolumner. Det här exemplet använder ”#genre” som uppslagsnamn och ”Genre” som kolumntiteln. Kolumntypen är ”Kommaseparerad text, som taggar, visas i taggbläddraren.”

Efter att du har startat om calibre måste du tala om för calibre att kolumnen ska behandlas som en hierarki. Gå till Inställningar → Utseende & känsla → Taggbläddrare och ange uppslagsnamnet ”#genre” i rutan ”Kategorier med hierarkiska poster”. Tryck på Tillämpa och du är klar inställningen.

För närvarande finns det inga genrer i kolumnen. Vi har kvar det sista steget: hur man tillämpar en genre på en bok. En genre finns inte i calibre förrän den visas på minst en bok. För att lära oss hur man tillämpar en genre för första gången måste vi gå in på detaljer om hur en genre ser ut i metadata för en bok.

En hierarki av ”saker” byggs genom att skapa en post som består av fraser åtskilda av perioder. Om du fortsätter med genreexemplet skulle dessa poster vara ”Historia.Militär”, ”Mysterier.Vampyr”, ”Science fiction.Rymdopera” o.s.v. För att skapa en ny genre väljer du en bok som ska ha den genren, redigerar dess metadata och ange den nya genren i den kolumn du skapade. Fortsättning på vårt exempel, om du vill tilldela en ny genre ”Serietidning” med en undergenre ”Superhjältar” till en bok, skulle du ”redigera metadata” för den boken (serietidningen), välj fliken Anpassade metadata och ange sedan ”Serietidning.Superhjältar” som visas i följande (ignorera de andra anpassade kolumnerna):

Efter att ha gjort ovanstående, ser du i taggbläddraren:

Från och med nu kan du antingen dra boken till genren eller lägga till den i boken med hjälp av redigera metadata på exakt samma sätt som gjorts ovan för att tillämpa den nya genren på en bok (en serietidning, förmodligen).

Observera

Hierarkisk visning fungerar bara om taggbläddraren är inställd på att sortera post efter namn. Detta är standard och kan kontrolleras genom att klicka på knappen Anpassa längst ner i taggbläddraren.

Söka

Det enklaste sättet att söka efter genrer är att använda taggbläddraren och klickar på den genre du vill se. Att klicka på en genre med underkategorier visar dig böcker med den genren och alla underkategorier. Detta kan dock ta upp en fråga. Bara för att en genre har underkategorier betyder inte att det inte är en genre i sin egen rätt. Till exempel kan en bok ha genren ”historia” men inte ”historia.militär”. Hur söker du efter böcker med endast ”historia”?

Taggbläddrarens sökmekanism vet om en post har underkategorier. Om den gör det, klicka du på posten utförs 5 sökningar istället för de normala tre. Den första är det normala gröna pluset, vilket bara visar böcker med denna genre (t.ex. historia). Den andra är ett dubbelt plus (visas ovan), vilket visar böcker med denna genre och alla undergenrer (t.ex. Historia och historia.militär). Den tredje är det normala röda minuset, vilket visar böcker utan den exakta genren. Den fjärde är ett fördubblad minus, vilket visar böcker utan den genren eller undergenrer. Den femte är tillbaka till början, ingen markering, vilket innebär ingen sökning.

Begränsningar

Om du söker efter en genre och sedan skapar en sparad sökning för den, kan du använda rutan ”begränsa till” för att skapa ett virtuellt bibliotek med böcker med den genren. Detta är användbart om du vill göra andra sökningar inom genren eller hantera/uppdatera metadata för böcker i genren. Om du fortsätter vårt exempel kan du skapa en Sparad sökning med namnet ”Historia.Japansk” genom att först klicka på genren Japanska i taggbläddraren för att få en sökning i sökfältet och ange Historia.Japansk i den sparade sökrutan och trycka sedan på knappen ”Spara sökning” (den gröna rutan med den vita pluset, på höger sida).

När du har skapat sparad sökning, kan du använda den som en begränsning.

Användbara mallfunktioner

Du kanske vill använda genreinformationen i en mall, till exempel med spara till disk eller överför till enhet. Frågan kan då vara ”Hur får jag det yttersta genrenamnet eller namnen? En calibre-mallfunktion, underposter, tillhandahålls för att göra det enklare.

Antag till exempel att du vill lägga till det yttersta genrenivån till spara-till-disk mall för att göra genre-mappar, som i ”Historia / The Gathering Storm - Churchill, Winston”. För att göra detta måste du extrahera den första nivån i hierarkin och lägga till den i fronten tillsammans med ett snedstreck för att ange att den ska skapa en mapp. Mallen nedan åstadkommer detta:

{#genre:subitems(0,1)||/}{title} - {authors}

Se mallspråket för mer information om mallar och funktionen subitems().

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- XPath-handledning

XPath-handledning

I den här handledningen kommer du att få en kort introduktion till XPath [https://sv.wikipedia.org/wiki/XPath], ett frågespråk som kan användas för att välja godtyckliga delar av HTML [https://sv.wikipedia.org/wiki/HTML]-dokument i calibre. XPath är en allmänt använd standard, och att googla det kommer att ge massor av information. Den här handledningen fokuserar däremot på att använda XPath för e-bokrelaterade uppgifter som att hitta kapitelrubriker i ett ostrukturerad HTML-dokument.

Innehåll

- Välja efter taggnamn

- Välja efter attribut

- Välja efter tagginnehåll

- Exempel e-bok

- XPath inbyggda funktioner

Välja efter taggnamn

Den enklaste formen av val är att välja taggar efter namn. Anta till exempel att du vill välja alla <h2>-taggar i ett dokument. XPath-förfrågan till detta är helt enkelt:

//h:h2 (Selects all <h2> tags)

Prefixet // betyder sökning på alla nivåer i dokumentet. Anta nu att du vill söka efter <span>-taggar som finns inuti <a>-taggar. Det kan uppnås med:

//h:a/h:span (Selects <span> tags inside <a> tags)

Om du vill söka efter taggar på en viss nivå i dokumentet, ändra prefixet:

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
children of the <body> tag)

Det här kommer endast att matcha <p>En mycket kort e-bok för att demonstrera användningen av XPath.</p> i Exempel e-bok men inte någon av de andra <p>-taggarna. Prefixet h: i exemplen ovan behövs för att matcha XHTML-taggar. Det här beror på att calibre internt representerar allt innehåll som XHTML. I XHTML har taggar ett namespace och h: är namespace-prefixet för HTML-taggar.

Anta nu att du vill välja både <h1>- och <h2>-taggar. För att göra det behöver vi en XPath-konstruktion som kallas predikat. Ett predikat är helt enkelt ett test som används för att välja taggar. Tester kan vara godtyckligt kraftfullt och som den här handledningen fortskrider, kommer du att se mer kraftfulla exempel. Ett predikat skapas genom att innesluta testuttrycket inom hakparenteser:

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

Det finns flera nya funktioner i detta XPath-uttryck. Den första är användningen av tecknet *. Det betyder matcha alla taggar. Titta nu på testuttrycket name()='h1' eller name()= 'h2'. name() är ett exempel på en inbyggd funktion. Den utvärderar helt enkelt namnet på taggen. Så genom att använda det kan vi välja taggar vars namn är antingen h1 eller h2. Observera att funktionen name() ignorerar namespaces så det inte finns något behov av prefixet h:. XPath har flera användbara inbyggda funktioner. Några fler kommer att introduceras i den här guiden.

Välja efter attribut

För att välja taggar baserat på deras attribut, användning av predikat krävs:

//*[@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")

Här hänvisar operatören @ till attributen för taggen. Du kan använda en del av de XPath inbyggda funktioner att utföra mer avancerad matchning på attributvärden.

Välja efter tagginnehåll

Med hjälp av XPath kan du även välja taggar som baseras på text de innehåller. Det bästa sättet att göra detta är att använda kraften i reguljära uttryck via den inbyggda funktionen re:test():

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

Här hänvisar .-operatören till innehållet i taggen, precis som @-operatören avser dess attribut.

Exempel e-bok

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

XPath inbyggda funktioner

name()

Namnet på den aktuella taggen.

contains()

contains(s1, s2) returnerar true om s1 innehåller s2.

re:test()

re:test(src, pattern, flags) returnerar true om strängen src matchar med det reguljära uttrycks mönster. En särskilt användbar flaggan är i, det gör matchning skiftlägesokänslig. En bra grund på syntaxen för reguljära uttryck finns på regexp syntax [https://docs.python.org/library/re.html]

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- Mallspråket för calibre

Mallspråket för calibre
calibre-mallspråket är ett calibre-specifikt språk som används i hela calibre för uppgifter som att specificera filsökvägar, formatera värden och beräkna värdet för användarspecificerade kolumner. Exempel:

- Ange mappstruktur och filnamn när du sparar filer från calibre-biblioteket till disken eller e-bokläsenhet.

- Definiera regler för att lägga till ikoner och färger till calibre-boklistan.

- Definiera ”virtuella kolumner” som innehåller data från andra kolumner.

- Avancerad bibliotekssökning.

- Avancerad metadata sök och ersätt.

Språket är uppbyggt kring föreställningen om en ”mall”, som specificerar vilka bokmetadata som ska användas, beräkningar på metadata och hur de ska formateras.

Grundläggande mallar

A basic template consists one or more template expressions. A template expression consists of text and names in curly brackets ({}) that is replaced by the corresponding metadata from the book being processed. For example, the default template in calibre used for saving books to device has 4 template expressions:

{author_sort}/{title}/{title} - {authors}

For the book ”The Foundation” by ”Isaac Asimov” the will become:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

The slashes are not template expressions because they are in between in {}. Such text is left where it appears. For example, if the template is:

{author_sort} Some Important Text {title}/{title} - {authors}

then for ”The Foundation” the template produces:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

A template expression can access all the metadata available in calibre, including custom columns (columns you create yourself), by using a column’s lookup name. To find the lookup name for a column (sometimes called fields), hover your mouse over the column header in calibre’s book list. Lookup names for custom columns always begin with #. For series type columns there is an additional field named #lookup name_index that is the series index for that book in the series. For example, if you have a custom series column named #myseries, there will also be a column named #myseries_index. The standard series column’s index is named series_index.

Utöver de vanliga kolumnbaserade fälten kan du också använda:

- {formats} - En lista över format tillgängliga i calibre-biblioteket för en bok

- {identifiers:select(isbn)} - Bokens ISBN

Om metadata för fältet för en given bok inte är definierad ersätts fältet i mallen med den tomma strängen (''). Tänk till exempel på följande mall:

{author_sort}/{series}/{title} {series_index}

Om Asimovs bok ”Second Foundation” finns i serien ”Foundation” så producerar mallen:

Asimov, Isaac/Foundation/Second Foundation 3

Om en serie inte har angetts för boken producerar mallen:

Asimov, Isaac/Second Foundation

Mallprocessorn tar automatiskt bort flera snedstreck och inledande eller efterföljande mellanslag.

Avancerad formatering

Förutom ersättning av metadata kan mallar villkorligt innehålla ytterligare text och styra hur ersatt data formateras.

Villkorligt inkludera text

Sometimes you want text to appear in the output only if a field is not empty. A common case is series and series_index where you want either nothing or the two values separated by a hyphen. calibre handles this case using a special template expression syntax.

For example and using the above Foundation example, assume you want the template to produce Foundation - 3 - Second Foundation. This template produces that output:

{series} - {series_index} - {title}

However, if a book has no series the template will produce - - the title, which is probably not what you want. Generally, people want the result be the title without the extraneous hyphens. You can accomplish this using the following template syntax:

{field:|prefix_text|suffix_text}

This template expression says that if field has the value XXXX then the result will be prefix_textXXXXXsuffix_text. If field is empty (has no value) then the result will be the empty string (nothing) because the prefix and suffix are ignored. The prefix and suffix can contain blanks.

Do not use subtemplates (`{ … }`) or functions (see below) in the prefix or the suffix.

Med hjälp av denna syntax kan vi lösa ovanstående icke-serier problem med mallen:

{series}{series_index:| - | - }{title}

Bindestreck inkluderas endast om boken har ett serieindex, vilket den bara har om den har en serie. Om du fortsätter med Foundation-exemplet igen kommer mallen att producera Foundation - 1 - Second Foundation.

Notes:

- You must include the colon after the lookup name if you are using a prefix or a suffix.

- You must either use either no or both | characters. Using one, as in {field:| - }, is not allowed.

- It is OK to provide no text for either the prefix or the suffix, such as in {series:|| - }. The template {title:||} is the same as {title}.

Formatering

Anta att du vill att serieindex ska formateras som tre siffror med inledande nollor. Detta gör tricket:

{series_index:0>3s} - Tre siffror med inledande nollor

För efterföljande nollor, använd:

{series_index:0<3s} - Tre siffror med efterföljande nollor

Om du använder serieindex med bråkvärden, t.ex. 1,1, kanske du vill att decimaltecken ska hamna i linje. Till exempel kanske du vill att indexen 1 och 2,5 ska visas som 01,00 och 02,50 så att de kommer att sortera korrekt på en enhet som gör lexikal sortering. För att göra detta, använd:

{series_index:0>5.2f} - Fem tecken som består av två siffror med inledande nollor, en decimalkomma, sedan 2 siffror efter decimalkomma.

Om du bara vill ha de två första bokstäverna i data, använd:

{author_sort:.2} - Endast de två första bokstäverna i författaren sorterar namn

Mycket av formateringen av calibre mallspråk kommer från Python. För mer information om syntaxen för dessa avancerade formateringsoperationer, se Python-dokumentationen [https://docs.python.org/3/library/string.html#formatstrings].

Använda mallar för att definiera anpassade kolumner

Templates can be used to display information that isn’t in calibre metadata, or to display metadata differently from calibre’s normal format. For example, you might want to show the ISBN, a field that calibre does not display. You can accomplish this creating a custom column with the type Column built from other columns (hereafter called composite columns) and providing a template to generate the displayed text. The column will display the result of evaluating the template. For example, to display the ISBN, create the column and enter {identifiers:select(isbn)} in the template box. To display a column containing the values of two series custom columns, separated by a comma, use {#series1:||,}{#series2}.

Sammansatta kolumner kan använda godtyckligt mallalternativ, även formatering.

Note: You cannot edit the data displayed in a composite column. Instead you edit the source columns. If you edit a composite column, for example by double-clicking it, calibre will open the template for editing, not the underlying data.

Mallar och pluggbrädor

Pluggbrädor används för att ändra metadata som skrivs in böcker under ”skicka till enhet” och ”spara till disk” operationer. En pluggbräda tillåter dig att ange en mall för att tillhandahålla data som ska skrivas i bokens metadata. Du kan använda pluggbrädor för att ändra följande fält: författare, author_sort, språk, förläggare, taggar, titel, title_sort. Den här funktion hjälper personer som vill använda olika metadata i böcker på enheter för att lösa sortering- eller visningsproblem.

When you create a plugboard, you specify the format and device for which the plugboard is to be used. A special device is provided, save_to_disk, that is used when saving formats (as opposed to sending them to a device). Once you have chosen the format and device, you choose the metadata fields to change, providing templates to supply the new values. These templates are connected to their destination fields, hence the name plugboards. You can of course use composite columns in these templates.

Plugboards are quite flexible and can be written in Single Function Mode, Template Program Mode, General Program Mode, or Python Template mode.

När en pluggbräda kan gälla (innehållsserver, spara till disk, eller skicka till enhet), söker calibre de definierade pluggbrädor att välja den rätta för givet format och enhet. Till exempel för att hitta rätt pluggbräda för en EPUB-bok som skickas till en Android-enhet, söker calibre dessa pluggbrädor enligt följande sökordning:

- en pluggbräda med en exakt matchning på format och enhet, till exempel EPUB och ANDROID

- en pluggbräda med en exakt matchning på format och den speciella any device val, till exempel `` EPUB`` och any device

- en pluggbräda med en exakt matchning på format och den speciella any device val, och ANDROID

- en pluggbräda med något format och någon enhet

Tagg och författar fält har särskild behandling, eftersom båda dessa fält kan ha mer än en post. En bok kan ha många taggar och många författare. När du anger att ett av dessa två fält ska ändras, är mallens resultat som undersöks för att se om mer än en post finns. För taggar, skärs resultatet bort varhelst calibre finner ett kommatecken. Till exempel om mallen producerar värdet Spänning, Skräck, då blir resultatet två taggar, Spänning och Skräck. Det finns inget sätt att sätta ett kommatecken i mitten av en tagg.

Samma sak händer för författare, men med en annan karaktär för snittet, en & (et-tecken) istället för ett kommatecken. Till exempel om mallen producerar värdet Blogs, Joe&Posts, Susan, då kommer boken att sluta med två författare, Blogs, Joe och Posts, Susan. Om mallen producerar värdet Blogs, Joe;Posts, Susan, då kommer boken att ha en författare med ett ganska konstigt namn.

Pluggbrädor påverkar metadata som skrivs in i boken när den sparas till disk eller skrivs till enheten. Pluggbrädor påverkar inte metadata som används av spara till disk och skicka till enhet för att skapa filnamnen. Istället konstrueras filnamn med hjälp av mallar som anges i lämpliga inställningsfönster.

Using functions in templates - Single Function Mode

Suppose you want to display the value of a field in upper case when that field is normally in title case. You can do this using template functions. For example, to display the title in upper case use the uppercase function, as in {title:uppercase()}. To display it in title case, use {title:titlecase()}.

Functions go into the format part of the template, after the : and before the first | or the closing } if no prefix/suffix is used. If you have both a format and a function reference, the function comes after a second :. Functions return the value of the column specified in the template, suitably modified.

Syntaxen för att använda funktioner är en av:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

Function names must always be followed by opening and closing parentheses. Some functions require extra values (arguments), and these go inside the parentheses. Arguments are separated by commas. Literal commas (commas as text, not argument separators) must be preceded by a backslash (\) . The last (or only) argument cannot contain a textual closing parenthesis.

Functions are evaluated before format specifications and the prefix/suffix. See further down for an example of using both a format and a function.

Viktigt: Om du har erfarenhet av programmering, notera att syntaxen i ”enkelt funktionsläge” inte är vad du förväntar dig. Strängar är inte citerade och mellanslag är betydande. Alla argument anses vara konstanter; det finns inga uttryck.

Do not use subtemplates (`{ … }`) as function arguments. Instead, use Template Program Mode and General Program Mode.

Some functions require regular expressions. In the template language regular expression matching is case-insensitive.

In the function documentation below, the notation [something]* means that something can be repeated zero or more times. The notation [something]+ means that the something is repeated one or more times (must exist at least one time).

The functions intended for use in Single Function Mode are:

capitalize() – returnerar värdet med den första bokstaven versaler och resten gemener.

contains(pattern, text if match, text if not match) – checks if the value is matched by the regular expression pattern. Returns text if match if the pattern matches the value, otherwise returns text if no match.

count(separator) – interprets the value as a list of items separated by separator and returns 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()

format_number(template) – interprets the value as a number and formats that number using a Python formatting template such as {0:5.2f} or {0:,d} or ${0:5,.2f}. The formatting template must begin with {0: and end with } as in the above examples. Exception: you can leave off the leading ”{0:” and trailing ”}” if the format template contains only a format. See the template language and the Python documentation [https://docs.python.org/3/library/string.html#formatstrings] for more examples. Returns the empty string if formatting fails.

human_readable() – förväntar sig att värdet är ett nummer och returnerar en sträng som representerar det numret i kB, MB, GB o.s.v.

ifempty(text if empty) – if the value is not empty then return the value of the field, otherwise return text if empty.

in_list(separator, [ pattern, found_val, ]* not_found_val) – interpret the value as a list of items separated by separator, checking the pattern against each item in the list. If the pattern matches an item then return found_val, otherwise return not_found_val. The pair pattern and found_value can be repeated as many times as desired, permitting returning different values depending on the item’s value. The patterns are checked in order, and the first match is returned.

language_strings(localize) – return the language names [https://www.loc.gov/standards/iso639-2/php/code_list.php] for the language codes [https://www.loc.gov/standards/iso639-2/php/code_list.php] passed in as the value. Example: {languages:language_strings()}. 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.

list_item(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 has the index -1 as in list_item(-1,separator). If the item is not in the list, then the empty string is returned.

lookup([ pattern, key, ]* else_key) – The patterns will be checked against the value in order. If a pattern matches then the value of the field named by key is returned. If no pattern matches then the value of the field named by else_key is returned. See``switch`` (below).

lowercase() – returnerar fältets värde med gemener.

rating_to_stars(use_half_stars) – Returns the rating as string of star (★) characters. The value must be a number between 0 and 5. Set use_half_stars to 1 if you want half star characters for fractional numbers available with custom ratings columns.

re(pattern, replacement) – return the value after applying the regular expression. All instances of pattern in the value are replaced with replacement. The template language uses case insensitive Python regular expressions [https://docs.python.org/3/library/re.html].

select(key) – interpret the value as a comma-separated list of items with each item having the form id:value (the calibre identifier format). The function finds the first pair with the id equal to key and returns the corresponding value. If no id matches then the function returns the empty string.

shorten(left chars, middle text, right chars) – Return a shortened version of the value, consisting of left chars characters from the beginning of the value, followed by middle text, followed by right chars characters from the end of the value. Left chars and right chars must be non-negative integers. Example: assume you want to display the title with a length of at most 15 characters in length. One template that does this is {title:shorten(9,-,5)}. For a book with the title Ancient English Laws in the Times of Ivanhoe the result will be Ancient E-anhoe: the first 9 characters of the title, a -, then the last 5 characters. If the value’s length is less than left chars + right chars + the length of middle text then the value will be returned unchanged. For example, the title The Dome would not be changed.

str_in_list(separator, [ string, found_val, ]+ not_found_val) – interpret the value as a list of items separated by separator then compare string against each value in the list. The string is not a regular expression. If string is equal to any item (ignoring case) then return the corresponding found_val. If string contains separators then it is also treated as a list and each subvalue is checked. The string and found_value pairs can be repeated as many times as desired, permitting returning different values depending on string’s value. If none of the strings match then not_found_value is returned. The strings are checked in order. The first match is returned.

subitems(start_index, end_index) – This function breaks apart lists of tag-like hierarchical items such as genres. It interprets the value as a comma-separated list of tag-like items, where each item is a period-separated list. It returns a new list made by extracting from each item the components from start_index to end_index, then merging the results back together. Duplicates are removed. The first subitem 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.

Exempel:

- Assuming a #genre column containing 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 column containing ”A.B.C, D.E”:

- {#genre:subitems(0,1)} returns ”A, D”

- {#genre:subitems(0,2)} returns ”A.B, D.E”

sublist(start_index, end_index, separator) – interpret the value as a list of items separated by separator, returning a new list made from the items from start_index to end_index. 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 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”

swap_around_articles(separator) – returns the value with articles moved to the end. The value can be a list, in which case each item in the list is processed. If the value is a list then you must provide the separator. If no separator is provided then the value is treated as being a single value, not a list. The articles are those used by calibre to generate the title_sort.

swap_around_comma() – 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 in the value then the function returns the value unchanged.

switch([pattern, value,]+ else_value) – for each pattern, value pair, checks if the value matches the regular expression pattern and if so returns the associated value. If no pattern matches, then else_value is returned. You can have as many pattern, value pairs as you wish. The first match is returned.

test(text if not empty, text if empty) – return text if not empty if the value is not empty, otherwise return text if empty.

titlecase() – returns the value of the field in title case.

transliterate() – Return a string in a latin alphabet formed by approximating the sound of the words in the source field. For example, if the source field is Фёдор Миха́йлович Достоевский this function returns Fiodor Mikhailovich Dostoievskii.

uppercase() – returns the value of the field in upper case.

Using functions and formatting in the same template

Suppose you have an integer custom column #myint that you want displayed with leading zeros, as in 003. One way to do this is to use a format of 0>3s. However, by default if a number (integer or float) equals zero then the value is displayed as the empty string so zero values will produce the empty string, not 000. If you want to see 000 values then you use both the format string and the ifempty function to change the empty value back to a zero. The template would be:

{#myint:0>3s:ifempty(0)}

Note that you can use the prefix and suffix as well. If you want the number to appear as [003] or [000], then use the template:

{#myint:0>3s:ifempty(0)|[|]}

General Program Mode

General Program Mode (GPM) replaces template expressions with a program written in the template language. The syntax of the language is defined by the following grammar:

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:

- a top_expression always has a value. The value of an expression_list is the value of the last top_expression in the list. For example, the value of the expression list 1;2;'foobar';3 is 3.

- In a logical context, any non-empty value is True

- In a logical context, the empty value is False

- Strings and numbers can be used interchangeably. For example, 10 and '10' are the same thing.

- Comments are lines starting with a ’#’ character. Comments beginning later in a line are not supported.

Operator precedence

The operator precedence (order of evaluation) from highest (evaluated first) to lowest (evaluated last) is:

- Funktionsanrop, konstanter, parentesuttryck, satsuttryck, tilldelningsuttryck, fältreferenser.

- Unary plus (+) and minus (-). These operators evaluate right to left.

These and all the other arithmetic operators return integers if the expression results in a fractional part equal to zero. For example, if an expression returns 3.0 it is changed to 3.

- Multiply (*) and divide (/). These operators are associative and evaluate left to right. Use parentheses if you want to change the order of evaluation.

- Add (+) and subtract (-). These operators are associative and evaluate left to right.

- Numeric and string comparisons. These operators return '1' if the comparison succeeds, otherwise the empty string (''). Comparisons are not associative: a < b < c is a syntax error.

- String concatenation (&). The & operator returns a string formed by concatenating the left-hand and right-hand expressions. Example: 'aaa' & 'bbb' returns 'aaabbb'. The operator is associative and evaluates left to right.

- Unary logical not (!). This operator returns '1' if the expression is False (evaluates to the empty string), otherwise ''.

- Logical and (&&). This operator returns ’1’ if both the left-hand and right-hand expressions are True, or the empty string '' if either is False. It is associative, evaluates left to right, and does short-circuiting [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html].

- Logical or (||). This operator returns '1' if either the left-hand or right-hand expression is True, or '' if both are False. It is associative, evaluates left to right, and does short-circuiting [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html]. It is an inclusive or, returning '1' if both the left- and right-hand expressions are True.

Field references

A field_reference evaluates to the value of the metadata field named by lookup name that follows the $ or $$. Using $ is equivalent to using the field() function. Using $$ is equivalent to using the raw_field function. Examples:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

If expressions

If expressions first evaluate the condition. If the condition is True (a non-empty value) then the expression_list in the then clause is evaluated. If it is False then if present the expression_list in the elif or else clause is evaluated. The elif and else parts are optional. The words if, then, elif, else, and fi are reserved; you cannot use them as identifier names. You can put newlines and white space wherever they make sense. The condition is a top_expression not an expression_list; semicolons are not allowed. The expression_lists are semicolon-separated sequences of top_expressions. An if expression returns the result of the last top_expression in the evaluated expression_list, or the empty string if no expression list was evaluated.

Examples:

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

Nested if example:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

As said above, an if produces a value. This means that all the following are equivalent:

* 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

As a last example, this program returns the value of the series column if the book has a series, otherwise the value of the title column:

program: field(if field('series') then 'series' else 'title' fi)

For expressions

The for expression iterates over a list of values, processing them one at a time. The list_expression must evaluate either to a metadata field lookup name e.g., tags or #genre, or to a list of values. The range() function (see below) generates a list of numbers. If the result is a valid lookup name then the field’s value is fetched and the separator specified for that field type is used. If the result isn’t a valid lookup name then it is assumed to be a list of values. The list is assumed to be separated by commas unless the optional keyword separator is supplied, in which case the list values must be separated by the result of evaluating the separator_expr. A separator cannot be used if the list is generated by range(). Each value in the list is assigned to the specified variable then the expression_list is evaluated. You can use break to jump out of the loop, and continue to jump to the beginning of the loop for the next iteration.

Example: This template removes the first hierarchical name for each value in Genre (#genre), constructing a list with the new names:

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

If the original Genre is History.Military, Science Fiction.Alternate History, ReadMe then the template returns Military, Alternate History, ReadMe. You could use this template in calibre’s Edit metadata in bulk  →  Search & replace with Search for set to template to strip off the first level of the hierarchy and assign the resulting value to Genre.

Observera: den sista raden i mallen, new_tags, är inte absolut nödvändig i det här fallet eftersom for returnerar värdet för det senaste topputtrycket i uttryckslistan. Värdet på en uppgift är värdet på dess uttryck, så värdet för satsen for är det som tilldelades new_tags.

Function definition

If you have code in a template that repeats then you can put that code into a local function. The def keyword starts the definition. It is followed by the function name, the argument list, then the code in the function. The function definition ends with the fed keyword.

Arguments are positional. When a function is called the supplied arguments are matched left to right against the defined parameters, with the value of the argument assigned to the parameter. It is an error to provide more arguments than defined parameters. Parameters can have default values, such as a = 25. If an argument is not supplied for that parameter then the default value is used, otherwise the parameter is set to the empty string.

The return statement can be used in a local function.

A function must be defined before it can be used.

Example: This template computes an approximate duration in years, months, and days from a number of days. The function to_plural() formats the computed values. Note that the example also uses the & operator:

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

Relational operators

Relational operators return '1' if the comparison is true, otherwise the empty string (’’).

There are two forms of relational operators: string comparisons and numeric comparisons.

String comparisons do case-insensitive string comparison using lexical order. The supported string comparison operators are ==, !=, <, <=, >, >=, in, and inlist.
For the in operator, the result of the left hand expression is interpreted as a regular expression pattern. The in operator is True if the value of left-hand regular expression matches the value of the right hand expression. The inlist operator is true if the left hand regular expression matches any one of the items in the right hand list where the items in the list are separated by commas. The matches are case-insensitive.

The numeric comparison operators are ==#, !=#, <#, <=#, >#, >=#. The left and right expressions must evaluate to numeric values with two exceptions: both the string value ”None” (undefined field) and the empty string evaluate to the value zero.

Exempel:

- program: field('series') == 'foo' returns '1' if the book’s series is ’foo’, otherwise ''.

- program: 'f.o' in field('series') returns '1' if the book’s series matches the regular expression f.o (e.g., foo, Off Onyx, etc.), otherwise ''.

- program: 'science' inlist field('#genre') returns '1' if any of the book’s genres match the regular expression science, e.g., Science, History of Science, Science Fiction etc., otherwise ''.

- program: '^science$' inlist field('#genre') returns '1' if any of the book’s genres exactly match the regular expression ^science$, e.g., Science. The genres History of Science and Science Fiction don’t match. If there isn’t a match then returns ''.

- program: if field('series') != 'foo' then 'bar' else 'mumble' fi returns 'bar' if the book’s series is not foo. Otherwise it returns 'mumble'.

- program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi returns 'yes' if series is either 'foo' or '1632', otherwise 'no'.

- program: if '^(foo|1632)$' in field('series') then 'yes' else 'no' fi returns 'yes' if series is either 'foo' or '1632', otherwise 'no'.

- program: if 11 > 2 then 'yes' else 'no' fi returns 'no' because the > operator does a lexical comparison.

- program: if 11 ># 2 then 'yes' else 'no' fi returns 'yes' because the ># operator does a numeric comparison.

Additional available functions

The following functions are available in addition to those described in Single Function Mode.

In GPM the functions described in Single Function Mode all require an additional first parameter specifying the value to operate upon. All parameters are expression_lists (see the grammar above).

add(x [, y]*) – returns the sum of its arguments. Throws an exception if an argument is not a number. In most cases you can use the + operator instead of this function.

and(value [, value]*) – returns the string ”1” if all values are not empty, otherwise returns the empty string. You can have as many values as you want. In most cases you can use the && operator instead of this function. One reason not to replace and with && is if short-circuiting can change the results because of side effects. For example, and(a='',b=5) will always do both assignments, where the && operator won’t do the second.

assign(id, val) – assigns val to id, then returns val. id must be an identifier, not an expression. In most cases you can use the = operator instead of this function.

approximate_formats() – return a comma-separated list of formats associated with the book. There is no guarantee that the list is correct, although it probably is. This and other zero-parameter functions can be called in Template Program Mode (see below) using the template {:'approximate_formats()'}. Note that resulting format names are always uppercase, as in EPUB. The approximate_formats() function is significantly faster than the formats_... functions discussed below.

author_links(val_separator, pair_separator) – returns a string containing a list of authors and those authors’ link values in the form:

author1 val_separator author1_link pair_separator author2 val_separator author2_link 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.

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 information (different from the author_sort in books). The returned list has the form author sort 1 val_separator author sort 2 etc. with no added spaces. 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 val_separator string.

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 and its companion book_values() are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak allow_template_database_functions_in_composites is set to True. It can be used only in the GUI.

For example this template search uses this function and its companion to find all series with only one book:

1. Define a stored template (using Preferences → Advanced → Template functions) named series_only_one_book (the name is arbitrary). The template is:

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, '')

The first time the template runs (the first book checked) it stores the results of the database lookups in a global template variable named vals. These results are used to check subsequent books without redoing the lookups.

2. Use the stored template in a template search:

template:"program: series_only_one_book()#@#:n:1"

Using a stored template instead of putting the template into the search eliminates problems caused by the requirement to escape quotes in search expressions.

book_values(column, query, sep, use_vl) – returns a list of the unique values contained in the column column (a lookup name), separated by sep, in the books found by searching for query. If use_vl is 0 (zero) then virtual libraries are ignored. This function and its companion book_count() are particularly useful in template searches, supporting searches that combine information from many books such as looking for series with only one book. It cannot be used in composite columns unless the tweak allow_template_database_functions_in_composites is set to True. It can be used only in the GUI.

booksize() – returnerar värdet för calibre ’storlek’-fältet. Returnerar ’’ om det inte finns några format.

check_yes_no(field_name, is_undefined, is_false, is_true) – checks if the value of the yes/no field named by the lookup name field_name is one of the values specified by the parameters, returning 'yes' if a match is found otherwise returning the 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 True or undefined (neither True nor False).

More than one of is_undefined, is_false, or is_true can be set to 1.

ceiling(x) – returns the smallest integer greater than or equal to x. Throws an exception if x is not a number.

character(character_name) – returns the character named by character_name. For example, character('newline') returns a newline character ('\n'). The supported character names are newline, return, tab, and backslash.

cmp(x, y, lt, eq, gt) – compares x and y after converting both to numbers. Returns lt if x <# y, eq if x ==# y, otherwise gt. This function can usually be replaced with one of the numeric compare operators (==#, <#, >#, etc).

connected_device_name(storage_location_key) – if a device is connected then return the device name, otherwise return the empty string. Each storage location on a device has its own device name. The storage_location_key names are 'main', 'carda' and 'cardb'. This function works only in the GUI.

connected_device_uuid(storage_location_key) – 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 storage_location_key location names are 'main', 'carda' and 'cardb'. This function works only in the GUI.

current_library_name() – return the last name on the path to the current calibre library.

current_library_path() – return the full path to the current calibre library.

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(). This function works only in the GUI.

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.

days_between(date1, date2) – returnera antalet dagar mellan date1 och date2. Numret är positivt om date1 är större än date2, annars negativt. Om någon date1 eller date2 inte är datum, returnerar funktionen tomma strängen.

divide(x, y) – returns x / y. Throws an exception if either x or y are not numbers. This function can usually be replaced by the / operator.

eval(string) – evaluates the string as a program, passing the local variables. This permits using the template processor to construct complex results from local variables. In Template Program Mode, because the { and } characters are interpreted before the template is evaluated 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.

extra_file_size(file_name) – returns the size in bytes of the extra file file_name in the book’s data/ folder if it exists, otherwise -1. See also the functions has_extra_files(), extra_file_names() and extra_file_modtime(). This function can be used only in the GUI.

extra_file_modtime(file_name, format_string) – returns the modification time of the extra file file_name in the book’s data/ folder if it exists, otherwise -1. The modtime is formatted according to format_string (see format_date() for details). If format_string is the empty string, returns the modtime as the floating point number of seconds since the epoch. See also the functions has_extra_files(), extra_file_names() and extra_file_size(). The epoch is OS dependent. This function can be used only in the GUI.

extra_file_names(sep [, pattern]) returns a sep-separated list of extra files in the book’s data/ 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. See also the functions has_extra_files(), extra_file_modtime() and extra_file_size(). This function can be used only in the GUI.

field(lookup_name) – returns the value of the metadata field with lookup name lookup_name.

field_exists(field_name) – checks if a field (column) with the lookup name field_name exists, returning '1' if so and the empty string if not.

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| - |- }. This function is provided to ease conversion of complex single-function- or template-program-mode templates to GPM Templates. For example, the following program produces the same output as the above template:

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Ytterligare exempel: för mallen {series:re(([^s])[^s]+(s|$),1)}{series_index:0>2s| - | - }{title}` använd:

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) – compares val < cmp in sequence, returning the associated result for the first comparison that succeeds. Returns else_result if no comparison succeeds. Example:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

returns "large". The same example with a first value of 16 returns "giant".

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.

floor(x) – returns the largest integer less than or equal to x. Throws an exception if x is not a number.

format_date(val, format_string) – format the value, which must be a date string, 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).

- åååå : året som fyrsiffrigt nummer.

- 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   : convert the date & time into a floating point number (a timestamp)

- from_number : konvertera ett flyttal (en tidsstämpel) till ett iso formaterat datum. Om du vill ha ett annat datumformat lägger du till önskad formateringssträng efter från_nummer och ett kolon (:). Exempel: från_nummer:MMM dd åååå

You might get unexpected results if the date you are formatting contains localized month names, which can happen if you changed the date format tweaks to contain MMMM. In this case, instead of using the field() function as in:

format_date(field('pubdate'), 'yyyy')

use the raw_field() function as in:

format_date(raw_field('pubdate'), 'yyyy')

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. Examples:

format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')

formats_modtimes(date_format_string) – return a comma-separated list of colon-separated items FMT:DATE representing modification times for the formats of a book. The date_format_string 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 modification time for a specific format. Note that format names are always uppercase, as in EPUB.

formats_paths() – return a comma-separated list of colon-separated items FMT:PATH giving the 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.

formats_sizes() – return a comma-separated list of colon-separated FMT:SIZE items giving the 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.

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.

get_link(field_name, field_value) – fetch the link for field field_name with value field_value. If there is no attached link, return the empty string. Examples:

- The following returns the link attached to the tag Fiction:

get_link('tags', 'Fiction')

- This template makes a list of the links for all the tags associated with a book in the form value:link, ...:

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() – return 'Yes' if the book has a cover, otherwise the empty string.

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. See also the functions extra_file_names(), extra_file_size() and extra_file_modtime(). This function can be used only in the GUI.

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 ('').

is_marked() – check whether the book is marked in calibre. If it is then return the value of the mark, either 'true' (lower case) or a comma-separated list of named marks. Returns '' (the empty string) if the book is not marked. This function works only in the GUI.

language_codes(lang_strings) – return the language codes [https://www.loc.gov/standards/iso639-2/php/code_list.php] for the language names passed in lang_strings. The strings must be in the language of the current locale. Lang_strings is a comma-separated list.

list_contains(value, separator, [ pattern, found_val, ]* not_found_val) – (Alias of in_list) Interpreting the value as a list of items separated by separator, evaluate the pattern against each value in the list. If the pattern matches any value then return found_val, otherwise return not_found_val. The pattern and found_value can be repeated as many times as desired, permitting returning different values depending on the search. The patterns are checked in order. The first match is returned. Aliases: in_list(), list_contains()

list_count(value, separator) – interprets value as a list of items separated by separator, returning the count of items in the list. Aliases: count(), list_count()

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

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.

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.

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.

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.

Exempel:

program:
list_join('#@#', $authors, '&', $tags, ',')

You can use list_join on the results of previous calls to list_join as follows:

program:
a = list_join('#@#', $authors, '&', $tags, ',');
b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

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:

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

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.

list_re_group(src_list, separator, include_re, search_re [, template_for_group]*) – Like list_re except replacements are not optional. It uses re_group(item, search_re, template ...) when doing the replacements.

list_remove_duplicates(list, separator) – return a list made by removing duplicate items in list. If items differ only in case then the last is returned. The items in list are separated by separator, as are the items in the returned list.

list_sort(list, direction, separator) – return list sorted using a case-insensitive lexical sort. If direction is zero, list is sorted ascending, otherwise descending. The list items are separated by separator, as are the items in the returned list.

list_split(list_val, sep, id_prefix) – splits list_val into separate values using sep, then assigns the values to local 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.

Exempel:

list_split('one:two:foo', ':', 'var')

is equivalent to:

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

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: merge_lists(), list_union()

mod(x, y) – returns the floor of the remainder of x / y. Throws an exception if either x or y is not a number.

multiply(x [, y]*) – returns the product of its arguments. Throws an exception if any argument is not a number. This function can usually be replaced by the * operator.

not(value) – returns the string ”1” if the value is empty, otherwise returns the empty string. This function can usually be replaced with the unary not (!) operator.

ondevice() – return the string 'Yes' if ondevice is set, otherwise return the empty string.

or(value [, value]*) – returns the string '1' if any value is not empty, otherwise returns the empty string. You can have as many values as you want. This function can usually be replaced by the || operator. A reason it cannot be replaced is if short-circuiting will change the results because of side effects.

print(a [, b]*) – prints the arguments to standard output. Unless you start calibre from the command line (calibre-debug -g), the output will go into a black hole. The print function always returns its first argument.

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

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 optional_default if the field’s value is undefined (None).

raw_list(lookup_name, separator) – returns the metadata list named by lookup_name without applying any formatting or sorting, with the items separated by separator.

re_group(value, pattern [, template_for_group]*) – return a string made by applying the regular expression pattern to value and replacing each matched instance with the value returned by the corresponding template. In Template Program Mode, like for the template and the eval functions, you use [[ for { and ]] for }.

The following example looks for a series with more than one word and uppercases the first word:

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

round(x) – returns the nearest integer to x. Throws an exception if x is not a number.

series_sort() – returnerar seriens sorterade värden.

strcat(a [, b]*) – can take any number of arguments. Returns a string formed by concatenating all the arguments.

strcat_max(max, string1 [, prefix2, string2]*) – Returns a string formed by concatenating the arguments. The returned value is initialized to string1. Strings made from prefix, string pairs are added to the end of the value as long as the resulting string length is less than max. Prefixes can be empty. Returns string1 even if string1 is longer than max. You can pass as many prefix, string pairs as you wish.

strcmp(x, y, lt, eq, gt) – does a case-insensitive lexical comparison of x and y. Returns lt if x < y, eq if x == y, otherwise gt. This function can often be replaced by one of the lexical comparison operators (==, >, <, etc.)

strcmpcase(x, y, lt, eq, gt) – does a case-sensitive lexical comparison of x and y. Returns lt if x < y, eq if x == y, otherwise gt.

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.

strlen(value) – Returns the length of the string value.

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

subtract(x, y) – returns x - y. Throws an exception if either x or y are not numbers. This function can usually be replaced by the - operator.

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.

today() – return a date+time string for today (now). 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 [https://en.wikipedia.org/wiki/ISO_8601] date/time format.

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.

to_hex(val) – returns the string val encoded in hex. This is useful when constructing calibre URLs.

urls_from_identifiers(identifiers, sort_results) – given a comma-separated list of identifiers, where an identifier is a colon-separated pair of values (id_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.

More complex programs in template expressions - Template Program Mode

Template Program Mode (TPM) is a blend of General Program Mode and Single Function Mode. TPM differs from Single Function Mode in that it permits writing template expressions that refer to other metadata fields, use nested functions, modify variables, and do arithmetic. It differs from General Program Mode in that the template is contained between { and } characters and doesn’t begin with the word program:. The program portion of the template is a General Program Mode expression list.

Example: assume you want a template to show the series for a book if it has one, otherwise show the value of a custom field #genre. You cannot do this in the Single Function Mode because you cannot make reference to another metadata field within a template expression. In TPM you can, as the following expression demonstrates:

{#series:'ifempty($, field('#genre'))'}

Exemplet visar flera saker:

- TPM is used if the expression begins with :' and ends with '}. Anything else is assumed to be in Single Function Mode.

- the variable $ stands for the field named in the template: the expression is operating upon, #series in this case.

- functions must be given all their arguments. There is no default value. For example, the standard built-in functions must be given an additional initial parameter indicating the source field.

- blanksteg ignoreras och kan användas var som helst i uttrycket.

- konstanta strängar omges av matchande citattecken, antingen ' eller ".

All the functions listed under Single Function Mode and General Program Mode can be used in TPM.

In TPM, using { and } characters in string literals can lead to errors or unexpected results because they confuse the template processor. It tries to treat them as template expression boundaries, not characters. In some but not all cases you can replace a { with [[ and a } with ]]. Generally, if your program contains { and } characters then you should use General Program Mode.

As with General Program Mode, for functions documented under Single Function Mode you must supply the value the function is to act upon as the first parameter in addition to the documented parameters. In TPM you can use $ to access the value specified by the lookup name for the template expression.

Python Template Mode

Python Template Mode (PTM) lets you write templates using native python and the calibre API [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. The database API will be of most use; further discussion is beyond the scope of this manual. PTM templates are faster and can do more complicated operations but you must know how to write code in python using the calibre API.

A PTM template begins with:

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'

You can add the above text to your template using the context menu, usually accessed with a right click. The comments are not significant and can be removed. You must use python indenting.

The context object supports str(context) that returns a string of the context’s contents, and context.attributes that returns a list of the attribute names in the context.

The context.funcs attribute allows calling Built-in and User template functions, and Stored GPM/Python templates, so that you can execute them directly in your code. The functions are retrieved using their names. If the name conflicts with a Python keyword, add an underscore to the end of the name. Examples:

context.funcs.list_re_group()
context.funcs.assert_()

Here is an example of a PTM template that produces a list of all the authors for a series. The list is stored in a Column built from other columns, behaves like tags. It shows in Book details and has the on separate lines checked (in Preferences → Look & feel → Book details). That option requires the list to be comma-separated. To satisfy that requirement the template converts commas in author names to semicolons then builds a comma-separated list of authors. The authors are then sorted, which is why the template uses 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))

The output in Book details looks like this:

Lagrade mallar

Both General Program Mode and Python Template Mode support saving templates and calling those templates from another template, much like calling stored functions. You save templates using Preferences → Advanced → Template functions. More information is provided in that dialog. You call a template the same way you call a function, passing positional arguments if desired. An argument can be any expression. Examples of calling a template, assuming the stored template is named foo:

- foo() – call the template passing no arguments.

- foo(a, b) call the template passing the values of the two variables a and b.

- foo(if field('series') then field('series_index') else 0 fi) – if the book has a series then pass the series_index, otherwise pass the value 0.

In GPM you retrieve the arguments passed in the call to the stored template using the arguments function. 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. For example, the following arguments function declares 2 variables, key, alternate:

arguments(key, alternate='series')

Examples, again assuming the stored template is named foo:

- foo('#myseries') – argument key is assigned the value 'myseries' and the argument alternate is assigned the default value 'series'.

- foo('series', '#genre') the variable key is assigned the value 'series' and the variable alternate is assigned the value '#genre'.

- foo() – the variable key is assigned the empty string and the variable alternate is assigned the value 'series'.

In PTM the arguments are passed in the arguments parameter, which is a list of strings. There isn’t any way to specify default values. You must check the length of the arguments list to be sure that the number of arguments is what you expect.

An easy way to test stored templates is using the Template tester dialog. For ease of access give it a keyboard shortcut in Preferences → Advanced → Keyboard shortcuts → Template tester. Giving the Stored templates dialog a shortcut will help switching more rapidly between the tester and editing the stored template’s source code.

Providing additional information to templates

A developer can choose to pass additional information to the template processor, such as application-specific book metadata or information about what the processor is being asked to do. A template can access this information and use it during the evaluation.

Developer: how to pass additional information

The additional information is a Python dictionary containing pairs variable_name: variable_value where the values must be strings. The template can access the dictionary, creating template local variables named variable_name containing the value variable_value. The user cannot change the name so it is best to use names that won’t collide with other template local variables, for example by prefixing the name with an underscore.

This dictionary is passed to the template processor (the formatter) using the named parameter global_vars=your_dict. The full method signature is:

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

Template writer: how to access the additional information

You access the additional information (the globals dictionary) in a template using the template function:

globals(id[=expression] [, id[=expression]]*)

where id is any legal variable name. This function checks whether the additional information provided by the developer contains the name. If it does then the function assigns the provided value to a template local variable with that name. If the name is not in the additional information and if an expression is provided, the expression is evaluated and the result is assigned to the local variable. If neither a value nor an expression is provided, the function assigns the empty string ('') to the local variable.

A template can set a value in the globals dictionary using the template function:

set_globals(id[=expression] [, id[=expression]]*)

This function sets the globals dictionary key:value pair id:value where value is the value of the template local variable id. If that local variable doesn’t exist then value is set to the result of evaluating expression.

Notes on the difference between modes

The three program modes, Single Function Mode (SFM), Template Program Mode (TPM), and General Program Mode (GPM), work differently. SFM is intended to be ’simple’ so it hides a lot of programming language bits.

Differences:

- In SFM the value of the column is always passed as an ’invisible’ first argument to a function included in the template.

- SFM doesn’t support the difference between variables and strings; all values are strings.

- The following SFM template returns either the series name or the string ”no series”:

{series:ifempty(no series)}

The equivalent template in TPM is

{series:'ifempty($, 'no series')'}

The equivalent template in GPM is:

program: ifempty(field('series'), 'no series')

The first argument to ifempty is the value of the field series. The second argument is the string no series. In SFM the first argument, the value of the field, is automatically passed (the invisible argument).

- Several template functions, for example booksize() and current_library_name(), take no arguments. Because of the ’invisible argument’ you cannot use these functions in SFM.

- Nested functions, where a function calls another function to compute an argument, cannot be used in SFM. For example this template, intended to return the first 5 characters of the series value uppercased, won’t work in SFM:

{series:uppercase(substr(0,5))}

- TPM and GPM support nested functions. The above template in TPM would be:

{series:'uppercase(substr($, 0,5))'}

I GPM skulle det vara:

program: uppercase(substr(field('series'), 0,5))

- As noted in the above Template Program Mode section, using { and } characters in TPM string literals can lead to errors or unexpected results because they confuse the template processor. It tries to treat them as template boundaries, not characters. In some but not all cases you can replace a { with [[ and a } with ]]. Generally, if your program contains { and } characters then you should use General Program Mode.

Användardefinierade Python-mallfunktioner

You can add your own Python functions to the template processor. Such functions can be used in any of the three template programming modes. The functions are added by going to Preferences  →  Advanced  →  Template functions. Instructions are shown in that dialog.

Särskilda anvisningar för att spara/skicka mallar

Särskild bearbetning tillämpas när en mall används i en spara på disk eller skicka till enhet mall. Värdena på fälten städas, ersätter tecken som är speciella till filsystem med understreck, inklusive snedstreck. Det innebär att textfältet inte kan användas för att skapa mappar. Snedstreck ändras dock inte i prefix eller suffixsträngar, så snedstreck i dessa strängar kommer att orsaka att mappar skapas. På grund av detta kan du skapa variabeldjup mappstruktur.

Antag till exempel att vi vill ha mappstrukturen series/series_index - title, med förbehållet att om serien inte finns, så bör titeln vara i översta mappen. Mallen för att göra detta är:

{series:||/}{series_index:|| - }{title}

Snedstrecket och bindestrecket visas bara om serien inte är tom.

The lookup function lets us do even fancier processing. For example, assume that if a book has a series, then we want the folder structure series/series index - title.fmt. If the book does not have a series then we want the folder structure genre/author_sort/title.fmt. If the book has no genre then we want to use ’Unknown’. We want two completely different paths, depending on the value of series.

För att åstadkomma det här, vi:

1. Create a composite field (give it lookup name #aa) containing {series}/{series_index} - {title}. If the series is not empty, then this template will produce series/series_index - title.

2. Create a composite field (give it lookup name #bb) containing {#genre:ifempty(Unknown)}/{author_sort}/{title}. This template produces genre/author_sort/title, where an empty genre is replaced with Unknown.

3. Set the save template to {series:lookup(.,#aa,#bb)}. This template chooses composite field #aa if series is not empty and composite field #bb if series is empty. We therefore have two completely different save paths, depending on whether or not series is empty.

Tips

- Use the Template Tester to test templates. Add the tester to the context menu for books in the library and/or give it a keyboard shortcut.

- Templates can use other templates by referencing composite columns built with the desired template. Alternatively, you can use Stored Templates.

- I en pluggbräda kan du ställa in ett fält till tomt (eller vad som motsvarar tomt) genom att använda den särskilda mallen {}. Den här mallen kommer alltid att utvärderas till en tom sträng.

- Den teknik som beskrivs ovan för att visa siffror även om de har ett nollvärde fungerar med standardfältet series_index.

Funktionsreferens

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(value [, value]*)
- not (värde)
- or(value [, value]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)
- days_between(date1, date2)
- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, format_string)

- 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_list(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, text if match, text if not match)

- field_exists(field_name)
- ifempty(val, text if empty)
- 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)

- tr_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(val, 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(med_avgränsare, lista1, avgränsare1 [, lista2, avgränsare2]*)

- 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(list, direction, separator)
- list_split(list_val, sep, id_prefix)
- list_union(list1, list2, separator)
- range(start, stopp, steg, gräns)
- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, separator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(template)

- 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(str, start, slut)
- swap_around_articles(val, 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- Mallspråket för 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(value [, value]*)

- not (värde)

- or(value [, value]*)

Date functions

- date_arithmetic(date, calc_spec, fmt)

- days_between(date1, date2)

- today()

Formatting values

- finish_formatting(val, fmt, prefix, suffix)

- format_date(val, format_string)

- 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_list(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, text if match, text if not match)

- field_exists(field_name)

- ifempty(val, text if empty)

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

- tr_in_list(val, separator, [string, found_val, ]+ not_found_val)

List manipulation

- count(val, 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(med_avgränsare, lista1, avgränsare1 [, lista2, avgränsare2]*)

- 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(list, direction, separator)

- list_split(list_val, sep, id_prefix)

- list_union(list1, list2, separator)

- range(start, stopp, steg, gräns)

- subitems(val, start_index, end_index)

- sublist(val, start_index, end_index, separator)

Other

- arguments(id[=expression] [, id[=expression]]*)

- assign(id, val)

- globals(id[=expression] [, id[=expression]]*)

- print(a[, b]*)

Recursion

- eval(template)

- 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(str, start, slut)

- swap_around_articles(val, 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]*) – returnerar summan av sina argument. Ger ett undantag om ett argument inte är ett tal. Denna funktion kan ofta ersättas med +-operatören.

ceiling(x)

class calibre.utils.formatter_functions.BuiltinCeiling[source]

ceiling(x) – returnerar det minsta heltalet större än eller lika med x. Ger ett undantag om x inte är ett tal.

divide(x, y)

class calibre.utils.formatter_functions.BuiltinDivide[source]

divide(x, y) – returnerar x / y. Ger ett undantag om antingen x eller y inte är siffror. Denna funktion kan ofta ersättas med operatören /.

floor(x)

class calibre.utils.formatter_functions.BuiltinFloor[source]

floor(x) – returnerar det största heltalet mindre än eller lika med x. Ger ett undantag om x inte är ett tal.

fractional_part(x)

class calibre.utils.formatter_functions.BuiltinFractionalPart[source]

fractional_part(x) – returnerar värdet efter decimalpunkten. Till exempel fractional_part(3.14) returnerar 0.14. Ger ett undantag om x inte är ett tal.

mod(x)

class calibre.utils.formatter_functions.BuiltinMod[source]

mod(x) – returneras floor(remainder of x / y). Kasta ett undantag om antingen x eller y inte är ett nummer.

multiply(x [, y]*)

class calibre.utils.formatter_functions.BuiltinMultiply[source]

multiply(x [, y]*) – returnerar produkten av sina argument. Ger ett undantag om något argument inte är ett tal. Denna funktion kan ofta ersättas med operatören *.

round(x)

class calibre.utils.formatter_functions.BuiltinRound[source]

round(x) – returnerar närmaste heltal till x. Ger ett undantag om x inte är ett tal.

subtract(x, y)

class calibre.utils.formatter_functions.BuiltinSubtract[source]

subtract(x, y) – returnerar x - y. Ger ett undantag om antingen x eller y inte är siffror. Denna funktion kan ofta ersättas med operatören -.

Boolean

and(value [, value]*)

class calibre.utils.formatter_functions.BuiltinAnd[source]

and(value [, value]*) – returnerar strängen ”1” om alla värden inte är tomma, annars returneras den tomma strängen. Denna funktion fungerar bra med test eller first_non_empty. Du kan ha så många värden som du vill. I många fall kan &&-operatören ersätta denna funktion.

not (värde)

class calibre.utils.formatter_functions.BuiltinNot[source]

not (värde) – returnerar strängen ”1” om värdet är tomt, annars returneras en tom sträng. Denna funktion fungerar bra med test eller first_non_empty. I många fall kan operatören ! ersätta denna funktion.

or(value [, value]*)

class calibre.utils.formatter_functions.BuiltinOr[source]

or(value [, value]*) – returnerar strängen ”1” om något värde inte är tomt, annars returneras en tom sträng. Denna funktion fungerar bra med test eller first_non_empty. Du kan ha så många värden som du vill. I många fall kan operatören || ersätta denna funktion.

Date functions

date_arithmetic(date, calc_spec, fmt)

class calibre.utils.formatter_functions.BuiltinDateArithmetic[source]

date_arithmetic(date, calc_spec, fmt) – Beräkna ett nytt datum från ’date’ med hjälp av ’calc_spec’. Returnera det nya datumet formaterat enligt valfritt ’fmt’: om det inte tillhandahålls kommer resultatet att vara i iso-format. calc_spec är en sträng som bildas genom att slå samman par av ’vW’ (valueWhat) där ’v’ är ett möjligen negativt tal och W är en av följande bokstäver: s: lägg till ’v’ sekunder till ’date’ m: lägg till ’v’ minuter till ’date’ h: lägg till ’v’ timmar till ’datum’ d: lägg till ’v’ dagar till ’date’ w: lägg till ’v’ veckor till ’data’ y: lägg till ’v’ år till ’date’, där ett år är 365 dagar. Exempel: ’1s3d-1m’ lägger till 1 sekund, lägger till 3 dagar och subtraherar 1 minut från ’data’.

days_between(date1, date2)

class calibre.utils.formatter_functions.BuiltinDaysBetween[source]

days_between(date1, date2) – returnerar antalet dagar mellan date1 och date2. Numret är positivt om date1 är större än date2, annars negativ. Om någon date1 eller date2 inte är datum, returnerar funktionen tomma strängen.

today()

class calibre.utils.formatter_functions.BuiltinToday[source]

today() – returnerar en datumsträng för idag. Detta värde är avsedd för användning i format_date eller days_between, men kan manipuleras som alla andra strängen. Datumet är i ISO format.

Formatting values

finish_formatting(val, fmt, prefix, suffix)

class calibre.utils.formatter_functions.BuiltinFinishFormatting[source]

finish_formatting(val, fmt, prefix, suffix) – tillämpa formatet, prefix och suffix till ett värde på samma sätt som görs i en mall som {series_index: 05.2f | - | -}. Till exempel ger följande program samma resultat som ovanstående mall: program: finish_formatting(field(”series_index”), ”05.2f”, ” - ”, ” - ”)

format_date(val, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDate[source]

format_date(val, format_string) – formatera värdet, som måste vara ett datum, genom att använda format_string, vilket returnerar en sträng. Formateringskoderna är: d : dagen som nummer utan inledande nolla (1 till 31) dd : dagen som nummer med inledande nolla (01 till 31) ddd : det förkortade lokaliserade dagsnamnet (t.ex. ”mån” till ”sön” ”). dddd : det långa lokaliserade dagsnamnet (t.ex. ”måndag” till ”söndag”). M : månaden som tal utan inledande nolla (1 till 12). MM : månaden som nummer med en inledande nolla (01 till 12) MMM : det förkortade lokaliserade månadsnamnet (t.ex. ”Jan” till ”Dec”). MMMM : det långa lokaliserade månadsnamnet (t.ex. ”januari” till ”december”). åå : året som tvåsiffrigt tal (00 till 99). åååå : året som fyrsiffrigt nummer. h : timmarna utan inledande 0 (0 till 11 eller 0 till 23, beroende på am/pm) hh : timmarna med inledande 0 (00 till 11 eller 00 till 23, beroende på am/pm) m : minuterna utan inledande 0 (0 till 59) mm : minuterna med inledande 0 (00 till 59) s : sekunderna utan inledande 0 (0 till 59) ss : sekunderna med inledande 0 (00 till 59) ap : använd en 12-timmarsklocka istället för en 24-timmarsklocka, med ”ap” ersatt av den lokaliserade strängen för am eller pm AP: använd en 12-timmarsklocka istället för en 24-timmarsklocka, med ”AP” ersatt av lokaliserad sträng för AM eller PM iso : datumet med tid och tidszon. Måste vara det enda formatet som finns till_tal: datumet som ett flyttal tal från_tal[:fmt]: formatera tidsstämpeln med fmt om det finns, annars iso

format_date_field(field_name, format_string)

class calibre.utils.formatter_functions.BuiltinFormatDateField[source]

format_date_field(field_name, format_string) – formatera värdet i fältet ’field_name’, som måste vara uppslagsnamnet för datumfältet, antingen standard eller anpassat. Se ”format_datum” för formateringskoderna. Den här funktionen är mycket snabbare än format_date och bör användas när du formaterar värdet i ett fält (kolumn). Det kan inte användas för beräknade datum eller datum i strängvariabler. Exempel: format_date_field(’pubdate’, ’åååå.MM.dd’)

format_number(v, template)

class calibre.utils.formatter_functions.BuiltinFormatNumber[source]

format_number(v, template) – format numret v med hjälp av en Python-formateringsmall som ”{0:5.2f}” eller ”{0:,d}” eller ”${0:5,.2f}”. field_name del av mallen ska vara en 0 (noll) ( ”{0:” i exemplen ovan). Se språkmallen och Python-dokumentationen för fler exempel. Du kan lämna den ledande ”{0:” och efterföljande ”}” om mallen innehåller ett format. Returnerar tom sträng om formatering misslyckas.

human_readable(v)

class calibre.utils.formatter_functions.BuiltinHumanReadable[source]

human_readable(v) – returnerar en sträng som representerar antalet v i KB, MB, GB osv.

rating_to_stars(value, use_half_stars)

class calibre.utils.formatter_functions.BuiltinRatingToStars[source]

rating_to_stars(value, use_half_stars) – Returnerar betygsättningen som sträng av stjärntecken. Värdet är ett tal mellan 0 och 5. Ställ in use_half_stars till 1 om du vill ha halvstjärntecken för anpassade betygskolumner som stöder icke-heltalsklassificering, till exempel 2.5.

urls_from_identifiers(identifiers, sort_results)

class calibre.utils.formatter_functions.BuiltinUrlsFromIdentifiers[source]

urls_from_identifiers(identifiers, sort_results) – ger en kommaseparerad lista med identifierare, där en identifierare är ett kolonseparerat värdepar (name:id_value), returnerar en kommaseparerad lista med HTML-URL:er genererade från identifierarna. Listan är inte sorterad om sort_results är 0 (tecken eller nummer), annars sorteras den alfabetiskt efter identifierarens namn. Webbadresserna genereras på samma sätt som kolumnen för inbyggda identifierare när de visas i bokdetaljer.

Get values from metadata

annotation_count()

class calibre.utils.formatter_functions.BuiltinAnnotationCount[source]

annotation_count() – returnera det totala antalet noteringar av alla typer som bifogas den aktuella boken. Den här funktionen fungerar bara i det grafiska gränssnittet.

approximate_formats()

class calibre.utils.formatter_functions.BuiltinApproximateFormats[source]

approximate_formats() – returnerar en kommaseparerad lista över format som vid ett tillfälle var i samband med boken. Det finns ingen garanti för att denna lista är korrekt, även om den antagligen är det. Denna funktion kan anropas i mallprogramläge med mallen ”{:’approximate_formats()’}”. Observera att formatnamnen alltid är versaler, som i EPUB. Den här funktionen fungerar bara i grafiska gränssnittet. Om du vill använda dessa värden på spara-till-disk eller skicka-till-enhet-mallar måste du göra en egen ”kolumn byggd från andra kolumner”, använder du funktionen i kolumnmallen och använda den kolumnens värde i din spar/skicka mallar

author_links(val_separator, pair_separator)

class calibre.utils.formatter_functions.BuiltinAuthorLinks[source]

author_links(val_separator, pair_separator) – returnerar en sträng som innehåller en lista med författare och denna författares länkvärde i formen author1 val_separator author1link pair_separator author2 val_separator author2link osv. En författare är separerad från dess länkvärde med val_separator sträng utan tillagda blanksteg. author:linkvalue par är separerade med pair_separator strängargument utan tillagda blanksteg.
Det är upp till dig att välja avgränsningssträngar som inte uppträder i författarnamn eller länkar. En författare är inkluderad även om författarlänken är tom.

author_sorts(val_separator)

class calibre.utils.formatter_functions.BuiltinAuthorSorts[source]

author_sorts(val_separator) – returnerar en sträng med en lista med författarens sorteringsvärden för författarna till boken. Sorteringen är ett i författarens metadata (som skiljer sig från den author_sort i böcker). Den återgivna listan har formen författaren sorterings 1 val_separator författare sort 2 osv. Författaren sortera värdena i denna lista är i samma ordning som författarna till boken. Om du vill ha utrymmen runt val_separator sedan inkludera dem i avgränsningssträngen

booksize()

class calibre.utils.formatter_functions.BuiltinBooksize[source]

booksize() – returnerar värdet av storleken fältet. Den här funktionen fungerar bara i grafiska gränssnittet. Om du vill använda detta värde i Save-to-disk eller skicka-till-enhet-mallar måste du göra en egen ”kolumn byggd från andra kolumner”, använder du funktionen i kolumnmallen och använda den kolumnens värde i din spara/skicka mallar

connected_device_name(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceName[source]

connected_device_name(storage_location) – om en enhet är ansluten, returnera enhetsnamnet, annars returnera den tomma strängen. Varje lagringsplats på en enhet kan ha olika namn. Platsnamnen är ”main”, ”carda” och ”cardb”. Den här funktionen fungerar bara i det grafiska gränssnittet.

connected_device_uuid(storage_location)

class calibre.utils.formatter_functions.BuiltinConnectedDeviceUUID[source]

connected_device_uuid(storage_location) – om en enhet är ansluten, returnera enhetens uuid (unikt id), annars returnera den tomma strängen. Varje lagringsplats på en enhet har olika hjälpmedel. Platsnamnen är ”main”, ”carda” och ”cardb”. Denna funktion fungerar endast i det grafiska gränssnittet.

current_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryName[source]

current_library_name() – returnerar efternamnet på sökvägen till det aktuella calibre-biblioteket. Den här funktionen kan anropas i mallprogramläget med mallen ”{:’current_library_name()’}”.

current_library_path ()

class calibre.utils.formatter_functions.BuiltinCurrentLibraryPath[source]

current_library_path () – returnerar sökvägen till det aktuella calibre-biblioteket. Den här funktionen kan anropas i mallprogramläget med ”{:’current_library_path ()’}”.

current_virtual_library_name()

class calibre.utils.formatter_functions.BuiltinCurrentVirtualLibraryName[source]

current_virtual_library_name() – returnera namnet på det aktuella virtuella biblioteket om det finns ett, annars den tomma strängen. Bibliotekets namn bevaras. Exempel: ”program: current_virtual_library_name()”.

field(lookup_name)

class calibre.utils.formatter_functions.BuiltinField[source]

field(lookup_name) – returnerar metadatafält med namnet efter lookup_name

formats_modtimes(date_format)

class calibre.utils.formatter_functions.BuiltinFormatsModtimes[source]

formats_modtimes(date_format) – returnerar en kommaseparerad lista över kolonseparerade post som representerar modifieringstider för formaten i en bok. Den date_format parametern anger hur datumet ska formateras. Se date_format funktionen för detaljer. Du kan använda funktionen för val för att få modifieringstider för ett visst format. Observera att formatnamn är alltid versaler, som i EPUB.

formats_paths()

class calibre.utils.formatter_functions.BuiltinFormatsPaths[source]

formats_paths() – returnerar en kommaseparerad lista över kolonseparerade poster som representerar fullständig sökväg till formaten i en bok. Du kan använda markeringsfunktionen för att få sökvägen för ett visst format. Observera att formatnamnen alltid är i versaler, som i EPUB.

formats_sizes()

class calibre.utils.formatter_functions.BuiltinFormatsSizes[source]

formats_sizes() – returnerar en kommaseparerad lista över kolonseparerade poster som representerar storlekar i oktetter av formaten i en bok. Du kan använda select-funktionen för att få storleken för ett visst format. Observera att formatnamn är alltid versaler, som i EPUB.

has_cover ()

class calibre.utils.formatter_functions.BuiltinHasCover[source]

has_cover () – returnera Ja om boken har ett omslag, annars returnera den tomma strängen

is_marked()

class calibre.utils.formatter_functions.BuiltinIsMarked[source]

is_marked() – kontrollera om boken är ’marked’ i calibre. Om det är så returneras värdet på markeringen, antingen ’true’ eller den kommaseparerade listan med namngivna markeringar. Returnerar ’’ om boken inte är markerad.

language_codes(lang_strings)

class calibre.utils.formatter_functions.BuiltinLanguageCodes[source]

language_codes(lang_strings) – returnerar språkkoderna för strängarna som finns i lang_strings. Strängarna ska vara på samma språk som den aktuella platsen. Lang_strings är en kommaseparerad lista.

language_strings(lang_codes, localize)

class calibre.utils.formatter_functions.BuiltinLanguageStrings[source]

language_strings(lang_codes, localize) – returnerar strängarna för språkkoder som finns i lang_codes. Om lokalisera är noll, returneras strängarna på engelska. Om lokalisera inte är noll, returneras strängarna tillbaka på språket på den aktuella platsen. Lang_codes är en kommaseparerad lista.

ondevice()

class calibre.utils.formatter_functions.BuiltinOndevice[source]

ondevice() – returnera Ja om ondevice är inställt, annars returneras den tomma strängen. Den här funktionen fungerar bara i grafiska gränssnittet. Om du vill använda detta värde i Save-to-disk eller skicka-till-enhet-mallar måste du göra en egen ”kolumn byggd från andra kolumner”, använder du funktionen i kolumnmallen och använder den kolumnen värde i din spara/skicka mallar

raw_field(lookup_name [, optional_default])

class calibre.utils.formatter_functions.BuiltinRawField[source]

raw_field(lookup_name [, optional_default]) – returnerar metadatafältet med lookup_name utan att använda någon formatering. Det utvärderar och returnerar det valfria andra argumentet ’default’ om fältet är odefinierat (’None’).

raw_list(lookup_name, separator)

class calibre.utils.formatter_functions.BuiltinRawList[source]

raw_list(lookup_name, separator) – returnerar metadatalistan namngiven av lookup_name utan att använda någon formatering eller sortering och med poster avskilda med avgränsare.

series_sort()

class calibre.utils.formatter_functions.BuiltinSeriesSort[source]

series_sort() – returnera seriesorteringsvärde

user_categories()

class calibre.utils.formatter_functions.BuiltinUserCategories[source]

user_categories() – returnerar en kommaseparerad lista över de användarkategorier som den här boken innehåller. Den här funktionen fungerar endast i det grafiska gränssnittet. Om du vill använda dessa värden i mallarna save-to-disk eller send-to-device måste du skapa en anpassad ”kolumn byggd från andra kolumner”, använd funktionen i den kolumnens mall och använda kolumnens värde i dina spara/skicka mallar

virtual_libraries()

class calibre.utils.formatter_functions.BuiltinVirtualLibraries[source]

virtual_libraries() – returnerar en kommaseparerad lista över virtuella bibliotek som innehåller den här boken. Den här funktionen fungerar bara i det grafiska gränssnittet. Om du vill använda dessa värden i mallarna spara-till-disk eller skicka-till-enhet måste du göra en anpassad ”Kolumn byggd från andra kolumner”, använda funktionen i kolumnens mall och använda den kolumnens värde i dina spara/skicka mallar

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) – kontrollerar värdet på fältet ja/nej som heter genom sökningsnyckelens field_name för ett värde som anges av parametrarna, returnerar ”ja” om en matchning hittas, annars returneras en tom sträng. Ställ in parametern is_undefined, is_false eller is_true till 1 (numret) för att kontrollera det villkoret, annars ställ in till 0. Exempel: check_yes_no(”# bool”, 1, 0, 1) returnerar ”ja” om fältet ja/nej ”#bool” är antingen odefinierad (varken sant eller falskt) eller sant. Mer än en av is_undefined, is_false eller is_true kan ställas in på 1. Den här funktionen används vanligtvis av funktionerna test() eller is_empty().

contains(val, pattern, text if match, text if not match)

class calibre.utils.formatter_functions.BuiltinContains[source]

contains(val, pattern, text if match, text if not match) – kontrollerar om val innehåller träffar för det reguljära uttrycket pattern ”mönster”. Returnerar text if match (”text om matchning”) om träffar hittas annars returneras text if no match (”text om ingen matchning”)

field_exists(field_name)

class calibre.utils.formatter_functions.BuiltinFieldExists[source]

field_exists(field_name) – kontrollerar om ett fält (kolumn) med namnet field_name finns, returnerar ”1” om så är fallet och ”” om inte.

ifempty(val, text if empty)

class calibre.utils.formatter_functions.BuiltinIfempty[source]

ifempty(val, text if empty) – returnerar val om val inte är tom, annars returnera text if empty

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) – returnerar text if empty om val inte är tomt, annars returneras text if empty

Iterating over values

first_non_empty(value [, value]*)

class calibre.utils.formatter_functions.BuiltinFirstNonEmpty[source]

first_non_empty(value [, value]*) – returnerar det första värdet som inte är tomt. Om alla värden är tomma, då returneras den tomma strängen. Du kan ha så många värden som du vill.

lookup(val, [pattern, field,]+ else_field)

class calibre.utils.formatter_functions.BuiltinLookup[source]

lookup(val, [pattern, field,]+ else_field) – som switch, förutom argumenten fält (metadata) namn, inte text. Värdet på lämpligt fält kommer att hämtas och användas. Observera att eftersom sammansatta kolumner är fält, kan du använda denna funktion i ett sammansatt fält och använda värdet av vissa andra sammansatta fältet. Detta är mycket användbart vid konstruerandet av variabla sparvägar

switch(val, [pattern, value,]+ else_value)

class calibre.utils.formatter_functions.BuiltinSwitch[source]

switch(val, [pattern, value,]+ else_value) – för varje pattern, value par, kontrollerar om fältet överensstämmer med det reguljära uttrycket pattern och i så fall returnerar det som`value`. Om inget mönster passar, då returneras else_value. Du kan ha så många pattern, value par som du vill

switch_if([test_expression, value_expression,]+ else_expression)

class calibre.utils.formatter_functions.BuiltinSwitchIf[source]

switch_if([test_expression, value_expression,]+ else_expression) – kontrollerar för varje ”test_expression, value_expression”-par om test_expression är True (icke-tomt) och returnerar i så fall resultatet av value_expression. Om inget test_expression är True returneras resultatet av else_expression. Du kan ha så många ”test_expression, value_expression”-par som du vill.

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]) – behandla val som en lista över identifierare separerade med kommatecken. En identifierare har formatet ”id_name:value”. Parametern id_name är texten id_name att söka efter, antingen ”id_name” eller ”id_name:regexp”. Det första fallet matchar om det finns någon identifierare som matchar det id_name. Det andra fallet matchar om id_name matchar en identifierare och regexp matchar identifierarens värde. Om found_val och not_found_val anges, om det finns en matchning, returnera found_val, annars returnera not_found_val. Om found_val och not_found_val inte tillhandahålls, om det finns en matchning, returnera identifier:value-paret, annars den tomma strängen.

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) – behandla val som en lista med objekt separerade med avgränsare, om mönstret matchar något av listvärdena returneras found_val. Om mönstret inte matchar något listvärde returneras not_found_val. Mönstret och found_value par kan upprepas så många gånger som önskas. Mönstren kontrolleras i ordning. found_val för den första matchningen returneras. Alias: in_list(), list_contains()

list_item(val, index, separator)

class calibre.utils.formatter_functions.BuiltinListitem[source]

list_item(val, index, separator) – tolka värdet som en lista med element åtskilda av separator, returnerar posten index`th. Den första punkten är siffran noll. Den sista komponenten kan returneras med hjälp av `list_item(-1,separator). Om posten inte finns i listan, kommer det tomma värdet returneras. Avgränsaren har samma betydelse som i antal-funktionen.

select(val, key)

class calibre.utils.formatter_functions.BuiltinSelect[source]

select(val, key) – tolka värdet som en kommaseparerad lista med poster, med posterna ”id:värde”. Hitta paret med id lika med tangent och returnera motsvarande värde. Returnerar den tomma strängen om ingen träff hittas.

tr_in_list(val, separator, [string, found_val, ]+ not_found_val)

class calibre.utils.formatter_functions.BuiltinStrInList[source]

tr_in_list(val, separator, [string, found_val, ]+ not_found_val) – behandlar val som en lista med objekt separerade med avgränsare, om strängen matchar något av listvärdena returneras found_val. Om strängen inte matchar något listvärde returneras not_found_val. Jämförelsen är exakt matchning (innehåller inte) och är skiftlägeskänslig. Paren string och found_value kan upprepas så många gånger som önskas. Mönstren kontrolleras i ordning. Found_val för den första matchningen returneras.

List manipulation

count(val, separator)

class calibre.utils.formatter_functions.BuiltinCount[source]

count(val, separator) – tolkar värdet som en lista med element åtskilda av separator, returnerar antalet poster i listan. De flesta listor använder kommatecken som skiljetecken, men författarna använder ett et-tecken. Exempel: {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) – tolkar ’list’ som en lista med poster som skiljs åt av ’separator’, returnerar antalet poster i listan som matchar det reguljära uttrycket ’pattern’. Alias: list_count_matching(), count_matching()

list_difference(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListDifference[source]

list_difference(list1, list2, separator) – returnerar en lista genom att ta bort list1 alla post som finns i list2 med en skiftlägeskänslig jämförelse. Posterna i list1 och list2 skiljs åt av avgränsare, liksom posterna i den återgivna listan.

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) – returnerar yes_val om list1 och list2 innehåller samma poster, annars returnera no_val. Föremålen bestäms genom att dela upp varje lista med lämpliga avgränsare (sep1 eller sep2). Ordningen på posterna i listorna är inte relevant. Jämförelsen är inte skiftlägeskänslig.

list_intersection(list1, list2, separator)

class calibre.utils.formatter_functions.BuiltinListIntersection[source]

list_intersection(list1, list2, separator) – returnerar en lista genom att ta bort list1 alla post som finns i list2 med en skiftlägeskänslig jämförelse. Posterna i list1 och list2 skiljs åt av avgränsare, liksom posterna i den återgivna listan.

list_join(med_avgränsare, lista1, avgränsare1 [, lista2, avgränsare2]*)

class calibre.utils.formatter_functions.BuiltinListJoin[source]

list_join(med_avgränsare, lista1, avgränsare1 [, lista2, avgränsare2]*) – returnera en lista gjord genom att sammanfoga posterna i källistorna (list1, osv.) med hjälp av with_separator mellan objekten i resultatlistan. Poster i varje källista[123…] separeras av den associerade avgränsaren[123…]. En lista kan innehålla nollvärden. Det kan vara ett fält som utgivare som är enskilt värderat, i praktiken en lista med en post. Dubbletter tas bort med en jämförelse som inte är skiftlägeskänslig. Poster returneras i den ordning de visas i källistorna. Om poster på listor skiljer sig endast i skiftläge används den sista. Alla avgränsare kan vara mer än ett tecken.
Exempel:

program:

list_join(’#@#’, $authors, ’&’, $tags, ’,’)

Du kan använda list_join på resultaten av tidigare anrop till list_join enligt följande:

program:

a = list_join(’#@#’, $authors, ’&’, $tags, ’,’); b = list_join(’#@#’, a, ’#@#’, $#genre, ’,’, $#people, ’&’)

Du kan använda uttryck för att skapa en lista. Anta till exempel att du vill ha objekt för författare och #genre, men med genren ändrad till ordet ’Genre: ’ följt av den första bokstaven i genren, dvs. genren ’Fiction’ blir ’Genre: F’. Följande kommer att göra det:

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) – Konstruera en lista genom att först separera src_list till poster med hjälp av avgränsartecknet. För varje post i listan, kontrollera om det matchar include_re. Om den gör det, sedan lägga till den i listan som ska returneras. Om opt_replace inte är den tomma strängen, tillämpa sedan bytet innan du lägger till posten i den återgivna listan.

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]+) – Som list_re utom ersättare är inte valfritt. Den använder re_group(list_item, search_re, group_template, …) när du gör ersättare på den resulterande listan.

list_remove_duplicates(list, separator)

class calibre.utils.formatter_functions.BuiltinListRemoveDuplicates[source]

list_remove_duplicates(list, separator) – returnerar en lista som gjorts genom att ta bort dubbletter i källistan. Om poster endast skiljer sig åt i skiftläge, returneras den sista av dem. Posterna i källistan är separerade med avgränsare, liksom posten i den återgivna listan.

list_sort(list, direction, separator)

class calibre.utils.formatter_functions.BuiltinListSort[source]

list_sort(list, direction, separator) – returnera en lista sorterad med en skiftlägesokänslig sortering. Om riktningen är noll sorteras listan stigande, annars fallande. Listposterna skiljs åt med avgränsare, liksom posterna i den återgivna listan.

list_split(list_val, sep, id_prefix)

class calibre.utils.formatter_functions.BuiltinListSplit[source]

list_split(list_val, sep, id_prefix) – delar list_val i separata värden med hjälp av ’sep’ och tilldelar sedan värdena till variabler med namnen ’id_prefix_N’ där N är positionen för värdet i listan. Den första posten har position 0 (noll). Funktionen returnerar det sista elementet i listan. Exempel: split(’one:two:foo’, ’:’, ’var’) motsvarar 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) – returnerar en lista som görs av en sammanslagning av poster i list1 och list2, ta bort dubbletter med en skiftlägeskänslig jämförelse. Om post skiljer sig i fallet är den i list1 används. Posterna i list1 och list2 skiljs åt av avgränsare, liksom posterna i den återgivna listan. Alias: list_union(), merge_lists()

range(start, stopp, steg, gräns)

class calibre.utils.formatter_functions.BuiltinRange[source]

range(start, stopp, steg, gräns) – returnerar en lista med siffror som genereras genom att slinga över det intervall som specificeras av parametrarna start, stopp och steg, med en maximal längd för gräns. Det första värdet som produceras är ’start’. Efterföljande värden nästa_v är current_v+steg. Slingan fortsätter medan nästa_v < stopp antar att steget är positivt, annars medan nästa_v > stopp. En tom lista skapas om start misslyckas i testet: start>=stopp om steget är positivt. Gränsen anger den maximala längden på listan och har en standard på 1000. Parametrarna start, steg och gräns är valfria. Att anropa range() med ett argument anger stop. Två argument anger start och stopp. Tre argument anger start, stopp och steg. Fyra argument anger start, stopp, steg och gräns. Exempel: 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) -> fel(gränsen har överskridits).

subitems(val, start_index, end_index)

class calibre.utils.formatter_functions.BuiltinSubitems[source]

subitems(val, start_index, end_index) – Den här funktionen används för att bryta isär listor över poster som genrer. Det tolkar värdet som en kommaseparerad lista över poster, där varje post är en period-separerad lista. Returnerar en ny lista genom att först hitta alla period-separerade poster, då för varje sådan post utvinna start_index till end_index komponenter och sedan kombinera resultaten tillbaka tillsammans. Den första komponenten i en periodseparerad lista har ett index på noll. Om ett index är negativ, så det räknas från slutet av listan. Som ett specialfall, är en end_index på noll antas vara längden på listan. Exempel med grundläggande malläge och antar ett #genre värde av ”A.B.C”: {#genre:subitems(0,1)} returnerar ”A”. {#genre:subitems(0,2)} returnerar ”A.B”. {#genre:subitems(1,0)} returnerar ”B.C”. Förutsatt ett #genre värde av ”A.B.C, D.E.F”, {#genre:subitems(0,1)} returnerar ”A, D”. {#genre:subitems(0,2)} returnerar ”A.B, D.E”

sublist(val, start_index, end_index, separator)

class calibre.utils.formatter_functions.BuiltinSublist[source]

sublist(val, start_index, end_index, separator) – tolka värdet som en lista av element separerade av separator, returnerar en ny lista med post från start_index till end_index. Det första postnumret är noll. Om ett index är negativt räknas det från slutet på listan. Som ett specialfall, om end_index är noll antas det motsvara längden på listan. Exempel på användning av grundläggande malläge och antagandet att (de kommaseparerade) taggkolumnerna innehåller ”A, B, C”: {tags:sublist(0,1,\,)} returnerar ”A”. {tags:sublist(-1,0,\,)} returnerar ”C”. {tags:sublist(0,-1,\,)} returnerar ”A, B”.

Other

arguments(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinArguments[source]

arguments(id[=expression] [, id[=expression]]*) – Används i en lagrad mall för att hämta argumenten som skickats i samtalet. Det både deklarerar och initialiserar lokala variabler, effektiva parametrar. Variablerna är positionella; de får värdet av parametern som ges i samtalet i samma position. Om motsvarande parameter inte tillhandahålls i samtalet tilldelar argumenten variabeln det angivna standardvärdet. Om det inte finns något standardvärde ställs variabeln in på den tomma strängen.

assign(id, val)

class calibre.utils.formatter_functions.BuiltinAssign[source]

assign(id, val) – tilldelar val till id och returnerar sedan val. id måste vara en identifierare, inte ett uttryck. Denna funktion kan ofta ersättas med operatören =.

globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinGlobals[source]

globals(id[=expression] [, id[=expression]]*) – Hämtar ”globala variabler” som kan skickas till formateraren. Den både deklarerar och initialiserar lokala variabler med namnen på de globala variablerna som skickats in. Om motsvarande variabel inte tillhandahålls i de skickade globalerna tilldelas den variabeln det angivna standardvärdet. Om det inte finns något standardvärde ställs variabeln in som den tomma strängen.

print(a[, b]*)

class calibre.utils.formatter_functions.BuiltinPrint[source]

print(a[, b]*) – skriver ut argument till standardutmatning. Om du inte startar calibre från kommandoraden (calibre-debug -g), går utmatningen till ett svart hål.

Recursion

eval(template)

class calibre.utils.formatter_functions.BuiltinEval[source]

eval(template) – utvärderar mallen, sänder de lokala variablerna (de ’assign’ed till) istället för bokens metadata. Detta möjliggör användning av mallprocessorn för att konstruera komplexa resultat från lokala variabler. Eftersom {- and }-tecken är speciella, måste du använda [[ för {-tecknet och ]] för }-tecknet; de konverteras automatiskt. Observera också att prefix och suffix (|prefix|suffix-syntax) inte kan användas i argumentet för denna funktion när du använder mallprogramläget.

template(x)

class calibre.utils.formatter_functions.BuiltinTemplate[source]

template(x) – utvärderar x som en mall. Utvärderingen görs i sitt eget sammanhang, vilket innebär att variabler inte delas mellan den som anropande och mallutvärderingen. Eftersom tecknen { och } är speciella, måste du använda [[ för {-tecknet och ]] för }-tecknet, och de konverteras automatiskt. Till exempel template(’[[title_sort]]’) kommer att utvärdera mallen {title_sort} och returnera dess värde. Observera också att prefix och suffix (|prefix|suffix-syntax) inte kan användas i argumentet för denna funktion när du använder mallprogramläget.

Relational

cmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinCmp[source]

cmp(x, y, lt, eq, gt) – jämför x och y efter konvertering av båda till siffror. Returnerar lt om x < y. Returnerar eq om x == y. Annars returneras gt. I många fall kan de numeriska jämförelseoperatörerna (>#, <#, ==# osv.) ersätta denna funktion.

first_matching_cmp(val, [cmp1, result1,]+, else_result)

class calibre.utils.formatter_functions.BuiltinFirstMatchingCmp[source]

first_matching_cmp(val, [cmp1, result1,]+, else_result) – jämför ”val < cmpN” i sekvens och returnerar resultN för den första jämförelsen som lyckas. Returnerar else_result om ingen jämförelse lyckas. Exempel: first_matching_cmp(10,5,”small”,10,”middle”,15,”large”,”giant”) returnerar ”large”. Samma exempel med ett första värde på 16 returnerar ”giant”.

strcmp(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmp[source]

strcmp(x, y, lt, eq, gt) – gör en skiftlägeskänslig jämförelse av x och y som strängar. Returnerar lt om x < y. Returnerar eq om x == y. Annars returneras gt. I många fall kan de lexikaliska jämförelseoperatorerna (>, <, == osv.) ersätta denna funktion.

strcmpcase(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmpcase[source]

strcmpcase(x, y, lt, eq, gt) – gör en skiftlägeskänslig jämförelse av x och y som strängar. Returnerar lt om x < y. Returnerar ekv om x == y. Annars returneras gt.
Obs: Detta är INTE standardbeteendet som används av calibre, till exempel i de lexikaliska jämförelseoperatorerna (==, >, <, osv.). Den här funktionen kan orsaka oväntade resultat, använd helst strcmp() när det är möjligt.

String case changes

capitalize(val)

class calibre.utils.formatter_functions.BuiltinCapitalize[source]

capitalize(val) – returnerar värdet av fältet med versaler

lowercase(val)

class calibre.utils.formatter_functions.BuiltinLowercase[source]

lowercase(val) – returnerar värdet i fältet med gemener

titlecase(val)

class calibre.utils.formatter_functions.BuiltinTitlecase[source]

titlecase(val) – returnerar värdet i fältet i titelformat

uppercase(val)

class calibre.utils.formatter_functions.BuiltinUppercase[source]

uppercase(val) – returnerar värdet i fältet med versaler

String manipulation

character(character_name)

class calibre.utils.formatter_functions.BuiltinCharacter[source]

character(character_name) – returnerar tecknet som heter character_name. Till exempel returnerar character(’newline’) ett nyradstecken (’n’). De tecken som stöds är ’newline’, ’return’, ’tab’ och ’backslash’.

re(val, pattern, replacement)

class calibre.utils.formatter_functions.BuiltinRe[source]

re(val, pattern, replacement) – Returnera val efter tillämpning av det reguljära uttrycket. Alla förekomster av pattern ersätts med replacement. Som allt i calibre är dessa Python-kompatibla reguljära uttryck

re_group(val, pattern [, template_for_group]*)

class calibre.utils.formatter_functions.BuiltinReGroup[source]

re_group(val, pattern [, template_for_group]*) – returnera en sträng gjord genom att tillämpa det reguljära uttrycksmönstret på val och ersätta varje matchad instans med strängen beräknad genom att ersätta varje matchad grupp med det värde som returneras av motsvarande mall. Det ursprungliga matchade värdet för gruppen finns som $. I mallprogramläge, som för mallen och eval-funktionerna, använder du [[ för { och ]] för }. Följande exempel i mallprogramläget letar efter serier med mer än ett ord och versaler det första ordet: {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) – Returnerar en förkortad version av fältet, som består av left chars, tecken från början av fältet, följt av middle text, följt av right chars tecken från slutet av strängen. Left chars och right chars, måste vara heltal. Antag till exempel titeln på boken är Ancient English Laws in the Times of Ivanhoe, och du vill att den ska passa i ett utrymme på högst 15 tecken. Om du använder {title:shorten(9,-,5)}, kommer resultatet att bli Ancient E-anhoe. Om fältets längd är mindre än left chars + right chars + längden på middle text, då kommer fältet att användas intakt. Till exempel titeln The Dome skulle inte ändras.

strcat(a [, b]*)

class calibre.utils.formatter_functions.BuiltinStrcat[source]

strcat(a [, b]*) – kan ta valfritt antal argument. Returnerar strängen som bildats genom att slå samman alla argument

strcat_max(max, string1 [, prefix2, string2]*)

class calibre.utils.formatter_functions.BuiltinStrcatMax[source]

strcat_max(max, string1 [, prefix2, string2]*) – Returnerar en sträng som bildats genom att slå samman argumenten. Det återgivna värdet initialiseras till string1. Prefix, string-par läggs till i slutet av värde så länge den resulterande strängens längd är mindre än max. String1 returneras även om string1 är längre än max. Du kan skicka så många prefix, string-par som du vill.

strlen(a)

class calibre.utils.formatter_functions.BuiltinStrlen[source]

strlen(a) – Returnerar längden av den sträng som skickas som argument

substr(str, start, slut)

class calibre.utils.formatter_functions.BuiltinSubstr[source]

substr(str, start, slut) – returnerar starttecknet till och med sluttecknet i str. Det första tecknet i STR är nollkaraktär. Om slutet är negativt, då det tyder på att för många tecken räknas från höger. Om slutet är noll, indikeras det sista tecknet. Till exempel substr (’12345 ’, 1, 0) returnerar ’2345’, och substr (’12345 ’, 1, -1) returnerar ’234’.

swap_around_articles(val, separator)

class calibre.utils.formatter_functions.BuiltinSwapAroundArticles[source]

swap_around_articles(val, separator) – returnerar valet med artiklar flyttade till slutet. Värdet kan vara en lista, i vilket fall varje medlem av listan behandlas. Om värdet är en lista måste du ange listvärdesseparatorn. Om ingen separator tillhandahålls behandlas värdet som ett enda värde, inte en lista.

swap_around_comma(val)

class calibre.utils.formatter_functions.BuiltinSwapAroundComma[source]

swap_around_comma(val) – givet ett värde av formen ”B, A” returnerar ”A B”. Det här är mest användbart för att konvertera namn i EN, FN-format till FN EN. Om det inte finns något kommatecken returnerar funktionen värdet oförändrat

to_hex(val)

class calibre.utils.formatter_functions.BuiltinToHex[source]

to_hex(val) – returnerar strängen kodad i hex. Det här är användbart när du skapar calibre-URL:er.

transliterate(a)

class calibre.utils.formatter_functions.BuiltinTransliterate[source]

transliterate(a) – Returnerar en sträng i ett latinskt alfabet som bildas genom en tillnärmning av ljudet av orden i källsträngen. Till exempel om källan är ”Фёдор Миха́йлович Достоевский” returnerar funktionen ”Fiodor Mikhailovich Dostoievskii”.

Template database functions

book_count(query, use_vl)

class calibre.utils.formatter_functions.BuiltinBookCount[source]

book_count(query, use_vl) – returnerar antalet böcker som hittats genom att söka efter förfråga. Om use_vl är 0 (noll) ignoreras virtuella bibliotek. Denna funktion kan endast användas i det grafiska användargränssnittet.

book_values(column, query, sep, use_vl)

class calibre.utils.formatter_functions.BuiltinBookValues[source]

book_values(column, query, sep, use_vl) – returnerar en lista över värdena som finns i kolumnen ”kolumn”, separerade med ”sep”, i böckerna som hittas genom att söka efter ”query”. Om use_vl är 0 (noll) ignoreras virtuella bibliotek. Denna funktion kan endast användas i det grafiska användargränssnittet.

extra_file_modtime(file_name, format_string)

class calibre.utils.formatter_functions.BuiltinExtraFileModtime[source]

extra_file_modtime(file_name, format_string) – returnerar ändringstiden för den extra filen ’file_name’ i bokens ’data/’-mapp om den finns, annars -1.0. Modtiden är formaterad enligt ’format_string’ (se format_date()). Om ’format_string’ är tom, returnerar modtiden som flyttal antal sekunder sedan epoken. Epok är OS-beroende. Denna funktion kan endast användas i det grafiska användargränssnittet.

extra_file_names(sep [, pattern])

class calibre.utils.formatter_functions.BuiltinExtraFileNames[source]

extra_file_names(sep [, pattern]) – returnerar en separat lista över extra filer i bokens ’data/’-mapp. Om den valfria parametern ’pattern’, ett reguljärt uttryck, tillhandahålls så filtreras listan till filer som matchar mönstret. Mönstermatchningen är skiftlägesokänslig. Denna funktion kan endast användas i det grafiska användargränssnittet.

extra_file_size(file_name)

class calibre.utils.formatter_functions.BuiltinExtraFileSize[source]

extra_file_size(file_name) – returnerar storleken i byte för den extra filen ’file_name’ i bokens ’data/’-mapp om den finns, annars -1. Denna funktion kan endast användas i det grafiska användargränssnittet.

get_link(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinGetLink[source]

get_link(field_name, field_value) – hämta länken för fältet ’field_name’ med värdet ’field_value’. Om det inte finns någon bifogad länk, returnera ’’. Exempel: get_link(’tags’, ’Fiction’) returnerar länken som är kopplad till taggen ’Fiction’.

get_note(field_name, field_value, plain_text)

class calibre.utils.formatter_functions.BuiltinGetNote[source]

get_note(field_name, field_value, plain_text) – hämta anteckningen för fältet ’fältnamn’ med värdet ’fältvärde’. Om ’plain_text’ är tom, returnera anteckningens HTML. Om ”plain_text” inte är tom, returnera anteckningens ren text. Om anteckningen inte finns, returnera ’’ i båda fallen. Exempel: get_note(’tags’, ’Fiction’, ’’) returnerar HTML-koden för anteckningen som är kopplad till taggen ’Fiction’.

has_extra_files([pattern])

class calibre.utils.formatter_functions.BuiltinHasExtraFiles[source]

has_extra_files([pattern]) – returnerar antalet extra filer, annars ’’ (den tomma strängen). Om den valfria parametern ’pattern’ (ett reguljärt uttryck) tillhandahålls så filtreras listan till filer som matchar mönstret innan filerna räknas. Mönstermatchningen är skiftlägesokänslig. Denna funktion kan endast användas i det grafiska användargränssnittet.

has_note(field_name, field_value)

class calibre.utils.formatter_functions.BuiltinHasNote[source]

has_note(field_name, field_value) – returnera ’1’ om värdet ’field_value’ i fältet ’field_name’ har en bifogad notering, annars ’’. Exempel: has_note(’tags’, ’Fiction’) returnerar ’1’ om taggen ’fiction’ har en bifogad anteckning, annars ’’.

other

set_globals(id[=expression] [, id[=expression]]*)

class calibre.utils.formatter_functions.BuiltinSetGlobals[source]

set_globals(id[=expression] [, id[=expression]]*) – Ställer in ”globala variabler” som kan skickas till formateraren. Globalerna ges namnet på det id som skickas in. Värdet på id:et används om inte ett uttryck tillhandahålls.

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=('Okänd',), 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »

- Allt om att använda reguljära uttryck i calibre

Allt om att använda reguljära uttryck i calibre

Reguljära uttryck är funktioner som används på många ställen i calibre för att utföra avancerad manipulation av e-bokens innehåll och metadata. Den här handledningen är en mild introduktion till att få dig igång med att använda vanliga uttryck i calibre.

Innehåll

- Först, ett varningens ord och ett modets ord

- Var i calibre kan man använda reguljära uttryck?

- Vad i hela världen är ett reguljärt uttryck?

- Har du lust att förklara?

- Det låter inte så illa. Vad kommer härnäst?

- Hej, snyggt! Detta börjar låta vettigt!

- Tja, dessa specialtecken är mycket snygga och allt, men vad händer om jag ville matcha en punkt eller ett frågetecken?

- Så, vilka är de användbaraste uppsättningarna?

- Men om jag hade några olika strängar jag ville matcha, blir saker komplicerade?

- Du missade…

- I början sa du att det fanns ett sätt att göra ett reguljärt uttryck skiftlägesokänsligt?

- Jag tror jag börjar förstå dessa reguljära uttryck nu… hur använder jag dem i calibre?

- Konverteringar

- Lägga till böcker

- Redigera metadata i grupp

- Snabbreferens

- Förtjänst

Först, ett varningens ord och ett modets ord

Detta kommer, oundvikligen, att vara något tekniska trots allt, reguljära uttryck är ett tekniskt verktyg för att göra tekniska saker. Jag kommer att behöva använda en del jargong och begrepp som kan verka komplicerat och invecklat. Jag ska försöka förklara dessa begrepp så tydligt som jag kan, men egentligen kan jag inte göra det utan att använda dem alla. Med detta sagt, förkasta inte detta på grund av all jargong, jag har försökt att förklara allt nytt. Och medan reguljära uttryck själva kan verka som en svårbegripliga, svart magi (eller för att vara mer prosaisk, en slumpad sträng av rappakalja bokstäver och tecken), jag lovar att det är inte så komplicerat. Även de som förstår vanliga uttryck riktigt bra kan ha problem med att läsa de mer komplexa, men att skriva dem är inte så svårt - du konstruerar uttrycket steg för steg. Så, ta ett steg och följa mig in i kaninhålet.

Var i calibre kan man använda reguljära uttryck?

Det finns ett fåtal platser calibre använder reguljära uttryck. Det finns Sök & ersätt i konverteringsalternativ, metadatadetektering från filnamn i importinställningar och Sök och ersätt när du redigerar metadata för böcker i grupp. calibres redigerare kan även använda reguljära uttryck i sin Sök och ersätt funktion. Slutligen kan du använda reguljära uttryck när du söker i calibre-boklistan och när du söker i calibres e-bokvisare.

Vad i hela världen är ett reguljärt uttryck?

Ett reguljärt uttryck är ett sätt att beskriva en uppsättningar strängar. En enda reguljära uttryck kan matcha flera olika strängar. Det är det som gör reguljära uttryck så kraftfulla – de är ett kortfattad sätt att beskriva ett potentiellt stort antal varianter.

Observera

Jag använder sträng här i den mening den används i programspråk: en sträng av ett eller flera tecken, tecken inklusive faktiska tecken, siffror, skiljetecken och s.k. whitespace (radbrytningar, tabulatorer o.s.v.). Observera att i allmänhet anses inte stora och små bokstäver vara samma, alltså ”a” som en annan karaktär från ”A” och så vidare. I calibre, är reguljära uttryck skiftlägesokänslig i sökfältet, men inte i konverteringsalternativet. Det finns ett sätt att göra alla reguljära uttryck fall okänsligt, men vi kommer att diskutera det senare. Det blir komplicerat eftersom reguljära uttryck tillåter variationer i strängar som matchar, så ett uttryck kan matcha flera strängar, vilket är varför folk bryr sig att använda dem. Mer om det om ett tag.

Har du lust att förklara?

Tja, det är därför vi är här. För det första är detta det viktigaste konceptet i reguljära uttryck: En sträng i sig är ett reguljärt uttryck som matchar sig själv. Det vill säga, om jag ville matcha strängen "Hej, Värld!" genom att använda ett reguljärt uttryck, skulle det reguljära uttrycket för att använda vara Hej, Värld!. Och ja, det är verkligen så enkelt. Du kommer att märka dock att detta endast matchar den exakta strängen "Hej, Värld!", inte t.ex. "Hej vÄrlden!" eller "hej, värld!" eller någon annan sådan variation.

Det låter inte så illa. Vad kommer härnäst?

Nästa är början på riktigt bra grejer. Kom ihåg var jag sa att reguljära uttryck kan matcha flera strängar? Det är här det blir lite mer komplicerat. Säg, som en något mer praktisk övning, e-boken du ville konvertera hade en otäck sidfot som räknar sidor, ”Sida 5 av 423”. Uppenbarligen skulle sidnumret stiga från 1 till 423, så du skulle behöva matcha 423 olika strängar, eller hur? Fel, faktiskt: reguljära uttryck tillåter dig definiera uppsättningar av tecken som matchar: För att definiera en uppsättning, lägger du alla de tecken du vill att ska vara i uppsättningen inom hakparenteser. Så, till exempel uppsättningen `` [abc]`` skulle matcha antingen tecknet ”a”, ”b” eller ”c”. Uppsättningarna kommer alltid bara matcha en av karaktärerna i uppsättningen. De ”förstår” teckenformattering, det vill säga om du ville matcha mot alla små bokstäver, skulle du använda uppsättningen [az] för gemener och för versaler skulle du använda `` [a-zA-Z]`` o.s.v. Förstår du principen? Så, självklart, med hjälp av uttrycket Sida [0-9] av 423 skulle du kunna matcha de första nio sidorna, vilket minskar uttrycken som behövs för tre: Det andra uttrycket Sida [0-9] [0-9] av 423 skulle matcha alla tvåsiffriga sidnummer, och jag är säker på att du kan gissa hur det tredje uttryck skulle se ut. Ja, gå vidare. Skriv ner det.

Hej, snyggt! Detta börjar låta vettigt!

Jag hoppades att du skulle säga det. Men håll i dig själv, nu blir det ännu bättre! Vi såg precis att med uppsättning kan vi matcha en av flera tecken samtidigt. Men du kan även upprepa ett tecken eller uppsättning, för att minska antalet uttryck som behövs för att hantera ovan sidnummerexemplet till ett. Ja, ETT! Upphetsad? Du borde vara det! Det fungerar så här: Några så kallade specialtecken ”+”, ”?” och ”*”, * upprepa det enda elementet som föregår dem*. (Element betyder antingen ett enda tecken, en teckenuppsättning, en flyktsekvens eller en grupp (vi ska lära oss om de två sista senare) - kort sagt vilken enskild enhet som helst i ett reguljärt uttryck). Dessa tecken kallas jokertecken eller kvantifierare. För att vara mer exakt, ”?” matchar 0 eller 1 förekomster av det föregående elementet, ”*” matchar 0 eller flera för det föregående elementet och ”+” matchar 1 eller flera av det föregående elementet. Några exempel: Uttrycket a? skulle matcha antingen ”” (som är den tomma strängen, inte strikt användbar i detta fall) eller ”a”, uttrycket a* skulle matcha ””, ”a”, ”aa” eller valfritt antal a i rad, och slutligen uttrycket a+ skulle matcha ”a”, ”aa” eller valfritt antal a i rad (Observera: det skulle inte matcha den tomma strängen!). Samma gäller för uppsättningar: Uttrycket `` [0-9]+`` skulle matcha varje heltal som finns! Jag vet vad du tänker, och du har rätt: Om du använder fallet med matchande sidnummer ovan, skulle inte det vara det enda uttrycket för att matcha alla sidnummer? Ja, uttrycket Page [0-9]+ of 423 skulle matcha varje sidnummer i den boken!

Observera

En kommentar om dessa kvantifierare: De försöker i allmänhet att matcha så mycket text som möjligt, så var försiktig när du använder dem. Detta kallas ”girigt beteende” - Jag är säker på att du förstår varför. Det blir problematiskt när man, säg, försöker matcha en tagg. Tänk dig, till exempel strängen "<p class =" calibre2">Titel här</p>" och låt oss säga att du skulle vilja matcha öppningstaggen (delen mellan det första paret vinkelfästen, lite mer om taggar senare). Man skulle kunna tro att uttrycket <p.*> skulle matcha den taggen men faktiskt, den matchar hela strängen! (Tecknet ”.” Är ett annat specialtecken. Den matchar något utom radbrytningar, så i grund och botten, uttrycket .* Skulle matcha vilken rad du än kan tänka dig). Försök istället att använda <p.*?> som gör kvantifierare "*" ogiriga. Detta uttryck skulle endast matcha den första öppningstaggen, som avsett. Det finns faktiskt ett annat sätt att åstadkomma detta: Uttrycket <p[^>]*> matchar denna öppningstagg- du kommer se varför efter nästa avsnitt. Bara konstatera att det ganska ofta finns mer än ett sätt att skriva ett reguljärt uttryck.

Tja, dessa specialtecken är mycket snygga och allt, men vad händer om jag ville matcha en punkt eller ett frågetecken?

Du kan självklart göra det: Lägg bara ett bakåtsnedstreck framför valfritt specialtecken och det tolkas som det bokstavliga tecknet, utan någon speciell betydelse. Detta par av ett bakåtsnedstreck följt av ett enstaka tecken kallas en flyktsekvensen, och handlingen att sätta ett bakåtsnedstreck framför ett specialtecken kallas ett flykttecken. En flyktsekvens tolkas som ett enda element. Det finns givetvis flyktsekvensener som gör mer än att bara flykttecken, till exempel betyder "\t" en tabulator. Vi kommer till några av flyktsekvenserna senare. Åh, och förresten, angående dessa specialtecken: Betrakta alla tecken som vi diskuterar i den här introduktionen som att de har någon speciell funktion och därför måste flyktmarkeras om du vill ha det bokstavliga tecknet.

Så, vilka är de användbaraste uppsättningarna?

Visste att du skulle fråga. Några användbara uppsättningar är [0-9] matchar en enda siffra, [a-z] matchar en enda gemen, [A-Z] matchar en enda versal, [a-zA-Z] matchar en enda bokstav och [a-zA-Z0-9] matchar en enda bokstav eller siffra. Du kan också använda en flyktsekvens som stenografi:

\d

motsvarar [0-9]

\w

motsvarar [a-zA-Z0-9_]

\s

motsvarar alla whitespace

Observera

”Whitespace” är en term för allt som inte kommer att skrivas ut. Dessa tecken inkluderar blanksteg, tabulator, radmatning, formulärmatning, vagnretur, icke-brytande blanksteg o.s.v.

Observera

Uppsättningarna med versaler och gemener kan matcha både versaler och gemener om inställningen för att göra sökningar okänsliga för skiftlägen är aktiverad. Sådana inställningar finns, till exempel i Inställningar->Sökning i själva calibre och i sökpanelen i verktyget calibre e-bokvisaren samt calibre-verktyget Redigera bok.

Som en sista anteckning om uppsättningar kan du också definiera en uppsättning som valfritt tecken utom de i uppsättningen. Du gör det genom att inkludera tecknet "^" som det allra första tecknet i uppsättningen. Således skulle [^a] matcha alla tecken utom ”a”. Det kallas att komplettera uppsättningen. Dessa flyktsekvensstenografier som vi såg tidigare kan också kompletteras: "\D" betyder vilket tecken som helst som inte är numeriskt, och är alltså ekvivalent med [^0-9]. De andra stenografierna kan kompletteras med, du gissade rätt, att använda respektive versaler istället för gemener. Så, om du går tillbaka till exemplet <p[^>]*> från föregående avsnitt, kan du nu se att teckenuppsättningen den använder försöker matcha vilket tecken som helst förutom en avslutande vinkelparentes.

Men om jag hade några olika strängar jag ville matcha, blir saker komplicerade?

Frukta inte, livet är fortfarande bra och enkelt. Tänk på detta exempel: Boken du konverterar har ”Titel” skrivet på varje udda sida och ”Författare” skrivet på varje jämn sida. Ser bra i tryck, eller hur? Men i e-böcker är det irriterande. Du kan gruppera hela uttryck i normala parenteser och tecknet "|" låter dig matcha antingen uttrycket till höger eller det till vänster. Kombinera dem så är du klar. För snabbt för dig? Okej, först och främst grupperar vi uttryck för udda och jämna sidor och på så sätt får (Titel)(Författare) som våra två nödvändiga uttryck. Nu gör vi det enklare genom att använda det lodräta strecket ("|" kallas vertikala stapeltecknet): Om du använder uttrycket (Titel|Författare) får du antingen en träff för ”Titel” (på udda sidorna) eller så får du en träff för ”Författare” (på jämna sidorna). Var det inte lätt?

Du kan naturligtvis använda det lodräta strecket utan att använda grupperingsparenteser, lika så. Kommer du ihåg när jag sa att kvantifierare upprepar elementet som föregår dem? Tja, det vertikala strecket fungerar lite annorlunda: Uttrycket ”Titel|Författare” kommer också att matcha antingen strängen ”Titel” eller strängen ”Författare”, precis som ovanstående exempel med hjälp av gruppering. Det vertikala strecket väljer mellan hela uttrycket före och efter den. Så, om du ville matcha strängarna ”Calibre” och ”calibre” och ville bara välja mellan det övre och små bokstäver ”c”, skulle du behöva använda uttrycket (c|C)alibre, där grupperingen säkerställer att endast ”c” kommer att väljas. Om du skulle använda c|Calibre, du skulle få en match på strängen ”c” eller på strängen ”Calibre”, vilket inte är vad vi ville ha. Kort sagt: Om du är osäker, använd gruppering tillsammans med det vertikala strecket.

Du missade…

… vänta lite, det finns en sista, riktigt snygg sak du kan göra med uppsättningar. Om du har en uppsättning som du tidigare matchade kan du använda referenser till uppsättningen senare i uttrycket: Uppsättningar är numrerade med början 1, och du refererar till dem genom att flyktnumret på den uppsättning som du vill referera till, alltså den femte gruppen skulle kallad \5. Så, om du sökte ([^]+)\1 i strängen ”Test Test”, skulle du matcha hela strängen!

I början sa du att det fanns ett sätt att göra ett reguljärt uttryck skiftlägesokänsligt?

Ja, det gjorde jag, tack för att uppmärksammande och att du påminde mig. Du kan berätta för calibre hur du vill vissa saker hanteras med hjälp av något som kallas flaggor. Du inkluderar flaggor i ditt uttryck med hjälp av speciella konstruktionen (?flaggor finns här) där, naturligtvis, skulle du ersätta ”flaggor finns här” med de specifika flaggor du vill ha. För att ignorera teckenskiftläge finns flaggan i därmed inkluderar du (?i) i ditt uttryck. Således (?i)test skulle matcha ”Test”, ”tEst”, ”TEst”, och varje fall variation du kan tänka dig.

En annan användbar flagga låter punkten matcha vilket tecken som helst, inklusive den nyrad, flaggan s. Om du vill använda flera flaggor i en sats, lägg dem bara i samma sats: (?is) skulle ignorera skiftläge och göra att punkten matchar alla. Det spelar ingen roll vilken flagga som du anger först, (?si) skulle motsvara ovanstående.

Jag tror jag börjar förstå dessa reguljära uttryck nu… hur använder jag dem i calibre?

Konverteringar

Låt oss börja med konverteringsinställningarna, vilket är riktigt snyggt. I Sök- och ersättsdel, kan mata in en regexp (kort för reguljära uttryck) som beskriver den sträng som kommer att ersättas under konverteringen. Den nätta delen är guiden. Klicka på guiden och du får en förhandsvisning av vad calibre ”ser” under konverteringen. Bläddra ner till den sträng som du vill ta bort, välj och kopiera den, klistra in den i regexp fältet högst upp i fönstret. Om det finns variabla delar, t.ex. sidnummer eller så använder uppsättningar och kvantifierare för att täcka dem, och medan du ändå håller på, kom ihåg att undkomma specialtecken, om det finns några. Klicka på knappen Test och calibre belyser delarna som skulle ersättas om du använder regexp. När du är nöjd, klicka på OK och konvertera. Var försiktig om din konverteringskällan har taggar som detta exempel:

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

(skamlöst utslitet från denna tråd [https://www.mobileread.com/forums/showthread.php?t=75594"]). Du skulle behöva ta bort en del av taggarna också. I detta exempel jag skulle rekommendera börjar med taggen <b class="calibre2">, nu måste du sluta med motsvarande sluttagg (öppningstaggar är <tag>, sluttaggar är </tag>), vilket är helt enkelt nästa </b> i detta fall. (Se en bra HTML-handboken eller fråga i forumet om du är osäker på den här punkten). Öppningstaggen kan vara beskrivas med <b.*?>, den avslutande taggen med hjälp av </b>, vilket vi kunde ta bort allt mellan dessa taggar med <b.*?>.*?</b>. Men att använda detta uttryck skulle vara en dålig idé, eftersom det tar bort allt omges av <b>- taggar (som förresten, återger den medföljande texten i fetstil), och det är en rimlig satsning som vi tar bort delar av boken på detta sätt. Istället inkluderar början av den medföljande strängen också, vilket gör det reguljära uttrycket <b.*?>\s*Skapa\s+genom\s+ABC\s+Amber\s+LIT.*?</b> Den \s med kvantifierare som ingår här i stället för explicit använda blanksteg som sedda i strängen för att fånga några varianter av strängen som kan uppstå. Kom ihåg att kontrollera vad calibre kommer att ta bort för att se till att du inte bort några delar som du vill behålla om du testar ett nytt uttryck. Om du bara kontrollera en händelse, kanske du missar en obalans någon annanstans i texten. Observera också att om du av misstag tar bort fler eller färre taggar än du faktiskt ville försöker calibre för att reparera den skadade koden efter att ha gjort borttagningen.

Lägga till böcker

En annan sak du kan använda reguljära uttryck för är att utvinna metadata från filnamn. Du hittar den här funktionen ”Lägga till böcker” i en del av inställningarna. Det finns en speciell funktion här: Du kan använda fältnamn för metadatafält, till exempel (?P<titel>) skulle indikera att calibre använder den här delen av strängen som boktitel. De tillåtna fältnamnen listas i fönstren, tillsammans med ett annat fint testfält. Ett exempel: Säg att du vill importera en hel massa filer som heter Klassiska texter: Den gudomliga komedin av Dante Alighieri.mobi. (Uppenbarligen finns detta redan i ditt bibliotek, eftersom vi alla älskar klassisk italiensk poesi) eller Science Fiction epos: Stiftelseserien av Isaac Asimov.epub. Detta är naturligtvis ett namngivningssystem som calibre inte kommer att utvinna några meningsfulla uppgifter ur - dess standarduttryck för att utvinna metadata är (?P<title>.+) - (?P<author>[^_]+). Ett reguljärt uttryck som fungerar här skulle vara [a-zA-Z]+: (?P<title>.+) by (?P<author>.+). Observera att i gruppen för metadatafältet måste du använda uttryck för att beskriva vad fältet faktiskt matchar. Och observera också att vid användning av testfältet calibre tillhandahåller måste du lägga till filändelsen till ditt testfilnamn, annars får du inga matchningar alls, trots att du använder ett fungerande uttryck.

Redigera metadata i grupp

Den sista delen är reguljära uttryck Sök och ersätt i metadatafält. Du kan komma åt detta genom att välja flera böcker i biblioteket och genom att redigera metadata i grupp. Var mycket försiktig när du använder den här sista funktionen, eftersom det kan göra mycket dåliga saker till ditt bibliotek! Dubbelkontrollera att dina uttryck verkligen gör vad du vill att de ska göra med hjälp av testfälten, och välj bara de böcker du verkligen vill ändra! I det sökläget för reguljära uttryck kan du söka i ett annat fält, ersätta texten med något och till och med skriva in resultatet i ett annat fält. Ett praktiskt exempel: Säg att ditt bibliotek innehöll böckerna i Frank Herberts Dune-serie, uppkallad efter stilen Dune 1 - Dune, Dune 2 - Dune Messiah och så vidare. Nu vill du få Dune in i seriefältet. Du kan göra det genom att söka efter \d+(*.?) -.* i titelfältet och ersätta det med \1 i seriefältet. Ser du vad jag gjorde där? Det är en referens till den första gruppen som du ersätter seriefältet med. Nu när du har serien klar behöver du bara göra en ny sökning efter .*? - i titelfältet och ersätta det med "" (en tom sträng), igen i titelfältet, och dina metadata är snygga och prydliga. Är inte det bra? Förresten, istället för att ersätta hela fältet kan du också lägga till eller låta föregå fältet, så om du ville att boktiteln ska infogas med serieinformation, kunde du göra det också. Som du vid det här laget utan tvekan har märkt, finns det en kryssruta markerad Skiftlägeskänslig, så att du inte behöver använda flaggor för att välja beteende här.

Nåväl, det avslutar nästan den mycket korta introduktionen till reguljära uttryck. Förhoppningsvis har jag ha visat dig tillräckligt för att åtminstone komma igång och så att du kan fortsätta lära dig själv - en bra utgångspunkt är Python-dokumentationen för reguljära uttryck [https://docs.python.org/library/re.html].

Ett sista ord av varning, dock: Regexps är kraftfulla, men också väldigt lätta att få fel. calibre ger riktigt bra testmöjligheter för att se om dina uttryck beter sig som du förväntar dig. Använd dem. Försök att inte skjuta dig själv i foten. (Gud, jag älskar det uttrycket…). Men skulle du, trots varningen, skada din fot (eller andra kroppsdelar), försök att lära dig av det.

Snabbreferens

- Snabbreferens för regexp-syntax

- Teckenklasser
- Stenografiska karaktärsklasser
- Kvantifierare
- Girig
- Alternering
- Uteslutning
- Ankaren
- Grupper
- Lookarounds
- Rekursion
- Speciella tecken
- Metatecken
- Lägen

Förtjänst

Tack för hjälpen med tips, rättningar och sådant:

- ldolse

- kovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

Mer information om regexps finns i Python-användarmanualen [https://docs.python.org/library/re.html]. Det verkliga reguljära uttrycksbiblioteket som används av calibre är: regex [https://bitbucket.org/mrabarnett/mrab-regex/src/hg/] som stöder flera användbara förbättringar över ett Python-standardbibliotek.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »

- Allt om att använda reguljära uttryck i calibre »

- Snabbreferens för regexp-syntax

Snabbreferens för regexp-syntax

Denna checklista sammanfattar de vanligaste/svåra att komma ihåg delar av regexp-motorn som finns i de flesta delar av calibre.

Innehåll

- Teckenklasser

- Stenografiska karaktärsklasser

- Kvantifierare

- Girig

- Alternering

- Uteslutning

- Ankaren

- Grupper

- Lookarounds

- Rekursion

- Speciella tecken

- Metatecken

- Lägen

Teckenklasser

Teckenklasser är användbara för att representera olika grupper av tecken, kortfattat.

Exempel:

Representation

Klass

[a-z]

Gemener. Innehåller inte tecken med accentmärke och ligaturer

[a-z0-9]

Gemener från a till z eller siffror från 0 till 9

[A-Za-z-]

Versaler eller gemener eller ett streck. För att inkludera bindestreck i en klass måste du lägga den i början eller i slutet för att inte förvirra den med bindestrecket som anger ett antal tecken

[^0-9]

Alla tecken utom en siffra. Insättningstecknet (^) placerad i början av klassen exkluderar tecknena i klassen (kompletterad klass)

[[a-z]--[aeiouy]]

Konsonanterna för gemener. En klass kan ingå i en klass. Tecknet -- exkluderar vad som följer dem

[\w--[\d_]]

Alla bokstäver (inklusive utländska accenttecken). Förkortade klasser kan användas inom en klass

Exempel:

<[^<>]+> to select an HTML tag

Stenografiska karaktärsklasser

Representation

Klass

\d

En siffra (samma som [0-9])

\D

Alla icke-numeriska tecken (samma som [^0-9])

\w

Ett alfanumeriskt tecken ([a-zA-Z0-9]) inklusive tecken med accentmärke och ligaturer

\W

Alla ”icke-ordet” tecken

\s

Blanksteg, icke-brytande blanksteg, tab, returrad

\S

Alla ”icke-whitespace” tecken

.

Alla tecken utom nyrad. Använd kryssrutan ”pricka alla” eller regexp-medifieraren (?s) för att inkludera nyradstecken.

Kvantifierare

Kvantifierare

** Antal förekomster av uttrycket som föregår kvantifieraren **

?

0 eller 1 förekomst av uttrycket. Samma som {0,1}

+

1 eller flera förekomster av uttrycket. Samma som {1,}

*

0, 1 eller flera förekomster av uttrycket. Samma som {0,}

{n}

Exakt n förekomster av uttrycket

{min,max}

Antal förekomster mellan de minsta och högsta värdena som ingår

{min,}

Antal förekomster mellan det lägsta värdet och det oändliga

{,max}

Antal förekomster mellan 0 och det högsta värdet som ingår

Girig

Som standard, med kvantifierare, är den reguljär uttrycksmotorn girig: den utökar valet så mycket som möjligt. Detta orsakar ofta överraskningar i början. ? följer en kvantifierare för att göra den lat. Undvik att sätta två i samma uttryck, resultatet kan vara oförutsägbart.

Se upp för häckande kvantifierare, till exempel mönstret (a*)* eftersom det exponentiellt ökar bearbetningstiden.

Alternering

Tecknet | i ett reguljärt uttryck är ett logiskt OR. Det betyder att antingen föregående eller följande uttryck kan matcha.

Uteslutning

Metod 1
pattern_to_exclude(*SKIP)(*FAIL)|pattern_to_select

Exempel:

"Blabla"(*SKIP)(*FAIL)|Blabla
väljer Blabla, i strängarna Blabla eller ”Blabla eller Blabla”, men inte i ”Blabla”.

Metod 2
pattern_to_exclude\K|(pattern_to_select)

"Blabla"\K|(Blabla)

väljer Blabla, i strängarna Blabla eller ”Blabla eller Blabla”, men inte i ”Blabla”.

Ankaren

Ett ankare är ett sätt att matcha en logisk plats i en sträng, snarare än ett tecken. De användbaraste ankare för textbehandling är:

\b

Betecknar en ordgräns, dvs. en övergång från utrymme till icke-utrymme tecken. Du kan till exempel använda``bsurd`` för att matcha the surd men inte absurd.

^

Matchar början av en rad (i flerradsläge, vilket är standard)

$

Matchar slutet av en rad (i flerradsläge, vilket är standard)

\K

Återställer startpositionen för valet till dess position i mönstret. Vissa regexp-motorer (men inte calibre) tillåter inte lookbehind av varierande längd, särskilt med kvantifierare. När du kan använda \K med dessa motorer låter det dig också bli av med denna gräns genom att skriva motsvarande ett positivt lookbehind variabel längd.

Grupper

(uttryck)

Fångstgrupp, som lagrar urvalet och kan återanropas senare i sök eller ersätt mönstren med \n, där n är sekvensnumret för fångstgruppen (som börjar vid 1 i läsordningen)

(?:expression)

Grupp som inte fångar markeringen

(?>expression)

Atomic Group: Så snart uttrycket är uppfyllt passerar regexp-motorn, och om resten av mönstret misslyckas kommer det inte att gå tillbaka för att testa andra kombinationer med uttrycket. Atomgrupper fångar inte.

(?|expression)

Gruppåterställningsgrupp: grenarna för alternationerna som ingår i uttrycket delar samma gruppnummer

(?<name>expression)

Grupp som heter ”namn”. Urvalet kan återanropas senare i sök-mönstret med (?P=namn) och i ersätt med \g<name>. Två olika grupper kan använda samma namn.

Lookarounds

Lookaround

Betydelse

?=

Positiv lookahead (placeras efter markeringen)

?!

Negativ lookahead (placeras efter markeringen)

?<=

Positiv lookbehind (placeras före markeringen)

?<!

Negativt lookbehind (placeras före markeringen)

Lookaheads och lookbehinds konsumerar inte tecken, de är noll långa och fångar inte. De är atomgrupper: så snart påståendet är tillfredsställt passerar regexp-motorn, och om resten av mönstret misslyckas kommer det inte att spåra inuti sökningen för att testa andra kombinationer.

När du letar efter flera matchningar i en sträng, vid startpositionen för varje matchförsök, kan en lookbehind inspektera tecknen före den aktuella positionen. Därför, på strängen 123, bör mönstret (?<=\d)\d (en siffra som föregås av en siffra) i teorin välja 2 och 3. Å andra sidan, \d\K\d kan bara välja 2, eftersom startpositionen efter det första valet är omedelbart före 3, och det inte finns tillräckligt med siffror för en andra match. På samma sätt fångar \d(\d) bara 2. I calibres regexp-motorövning beter sig det positiva utseendet på samma sätt och väljer endast 2, i motsats till teorin.

Grupper kan placeras i lookarounds, men fånga är sällan användbart. Icke desto mindre, om det är användbart, kommer det att vara nödvändigt att vara mycket försiktig när man använder en kvantifierare i en lookbehind: girigheten som är associerad med frånvaron av backtracking kan ge en överraskande fångst. Använd därför \K i stället för en positiv lookbehind när du har en kvantifierare (eller värre, flera) i en fångande grupp av den positiva lookbehind.

Exempel på negativ lookahead:

(?![^<>{}]*[>}])

Placeras i slutet av mönstret förhindrar att välja inom en tagg eller ett format inbäddad i filen.

När det är möjligt är det alltid bättre att ”förankra” lookarounds, för att minska antalet steg som krävs för att få resultatet.

Rekursion

Representation

Betydelse

(?R)

Rekursion av hela mönstret

(?1)

Rekursion av det enda mönstret i den numrerade fångstgruppen, här grupp 1

Rekursion anropar sig själv. Detta är användbart för balanserade förfrågningar, till exempel citerade strängar, som kan innehålla inbäddade citattecken. Således, om vi under bearbetningen av en sträng mellan dubbla citattecken stöter på början av en ny sträng mellan dubbla citattecken, vet vi hur vi ska göra, och vi anropar oss själva. Då har vi ett mönster som:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

För att välja en sträng mellan dubbla citattecken utan att stanna på en inbäddad sträng:

“((?>[^“”]+|(?R))*[^“”]+)”

Denna mall kan också användas för att ändra par av taggar som kan bäddas in, till exempel <div> taggar.

Speciella tecken

Representation

Tecken

\t

tabulering

\n

radbrytning

\x20

(brytbart) mellanrum

\xa0

icke-brytbart mellanrum

Metatecken

Metatecken är de som har en speciell betydelse för regexp-motorn. Av dessa måste tolv föregås av ett escape-tecken, bakstrecket (\), för att förlora sin speciella mening och bli ett vanligt tecken igen:

^ . [ ] $ ( ) * + ? | \

Sju andra metatecken behöver inte föregås av ett bakstreck (men kan vara utan någon annan konsekvens):

{ } ! < > = :

Specialtecken förlorar sin status om de används i en klass (mellan klamrar []). Stängningsklammern och bindestrecket har en särskild status i en klass. Utanför klassen är bindestrecket en enkel bokstav, stängningsklammern förblir ett metatecken.

Snedstreck (/) och nummertecken (eller hashtecken) (#) är inte metatecken, de behöver inte undgås.

I vissa verktyg, som regex101.com med Python-motorn, har dubbla citattecken den speciella statusen för avskiljare och måste rymmas, eller alternativen ändras. Detta är inte fallet i redigeraren för calibre.

Lägen

(?s)

Orsakar punkten (.) att matcha nyradstecken också

(?m)

Gör så att ankarna ^ och $ matchar början och slutet av rader istället för början och slutet av hela strängen.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »

- Skriv dina egna insticksmoduler för att utöka calibres funktionalitet

Skriv dina egna insticksmoduler för att utöka calibres funktionalitet
calibre är en väldigt modulär konstruktion. Nästan alla funktioner i calibre kommer i form av insticksmoduler. Insticksmoduler används för konvertering, för hämtning av nyheter (även om dessa kallas för recept), för diverse komponenter av användargränssnitt, för anslutning av olika enheter, för bearbetning av filer när de läggs till calibre och så vidare. Du kan få en komplett lista med alla inbyggda insticksmoduler i calibre genom att gå till Inställningar → Avancerat → Insticksmoduler.

Här kommer vi att lära dig att skapa dina egna insticksmoduler för att lägga till nya funktioner i calibre.

Innehåll

- Anatomi av en calibre-insticksmodul

- En användargränssnitts insticksmodul

- __init__.py

- ui.py

- main.py

- Få resurser från insticksmodulens ZIP-fil

- Aktivera användarkonfiguration för din insticksmodul

- Redigera bokinsticksmodul

- main.py

- Lägga till översättningar till din insticksmodul

- Insticksmoduls API

- Felsöka insticksmodul

- Fler insticksmodul exempel

- Dela dina insticksmodul med andra

Observera

Detta gäller bara calibre-utgåvor >= 0.8.60

Anatomi av en calibre-insticksmodul

En calibre-insticksmodul är väldigt enkel, det är bara en ZIP-fil som innehåller lite Python-kod och andra resurser som bildfiler som behövs av insticksmodulen. Utan vidare, låt oss se ett grundläggande exempel.

Anta att du har en installation av calibre som du använder för att själv publicera olika e-dokument i EPUB- och MOBI-format. Du vill att alla filer som skapas av calibre har förläggaren inställd som ”Hej världen”, här är hur man gör det. Skapa en fil med namnet __init __ py (detta är ett speciellt namn och ska alltid användas för huvudfilen för din insticksmodul) och ange följande Python-kod i den:

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

Det är allt. För att lägga till den här koden i calibre som en insticksmodul, kör helt enkelt följande i mappen där du skapade __init__.py:

calibre-customize -b .

Observera

På macOS finns kommandoradsverktygen inuti calibre paketet, till exempel om du installerade calibre i /Applications kommandoradsverktygen finns i /Applications/calibre.app/Contents/MacOS/.

Du kan hämta insticksmodulen Hello World från helloworld_plugin.zip [https://calibre-ebook.com/downloads/helloworld_plugin.zip].

Varje gång du använder calibre att konvertera en bok, kommer insticksmodulens run() metod att anropas och den konverterade boken kommer att ha sin förläggare satt till ”Hello World”. Denna är en trivial insticksmodul, låt oss gå vidare till ett mer komplext exempel som faktiskt lägger till en komponent till användargränssnittet.

En användargränssnitts insticksmodul

Denna insticksmodul kommer att spridas över några filer (för att hålla koden ren). Den kommer att visa dig hur du får resurser (bilder eller datafiler) från insticksmodulens ZIP-fil, tillåter användare att anpassa din insticksmodul, hur man skapar element i calibre-användargränssnittet och hur man får åtkomst till och söker i bokdatabasen i calibre.

Du kan hämta denna insticksmodul från interface_demo_plugin.zip [https://calibre-ebook.com/downloads/interface_demo_plugin.zip]

Det första att observera är att denna ZIP-filen har många fler filer i sig, som förklaras nedan, ta särskild hänsyn till plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

En tom textfil som används för att aktivera insticksmodulsmagin för flera filer. Den här filen måste finnas i alla insticksmoduler som använder mer än en .py-fil. Den ska vara tom och filnamnet måste ha formen: plugin-import-name-**some_name**.txt. Förekomsten av den här filen låter dig importera kod från .py-filerna som finns i ZIP-filen med en sats som:

from calibre_plugins.some_name.some_module import some_object

Prefixet calibre_plugins måste alltid vara närvarande. some_name kommer från filnamnet på den tomma textfilen. some_module avser some_module.py fil inuti ZIP-filen. Observera att denna import är lika kraftfull som vanlig Python importering. Du kan skapa paket och underpaket för .py moduler inuti ZIP filen, precis som du normalt skulle (genom att definiera __init__.py i varje undermapp), och allt ska bara fungera.

Namnet du använda för some_name går in i en globala namnrymden delas av alla insticksmodul, så gör det så unikt som möjligt. Men kom ihåg att det måste vara en giltigt Python-identifierare (endast alfabet, siffror och understreck).

__init__.py

Som tidigare, filen som definierar insticksmodulsklassen

main.py

Den här filen innehåller den faktiska koden som gör något användbart

ui.py

Denna fil definierar gränssnittsdelen av insticksmodulen

bilder/ikon.png

Ikonen för denna insticksmodul

about.txt

En textfil med information om insticksmodulen

översättningar

En mapp innehåller .mo-filer med översättningar av användargränssnittet av din insticksmodul till olika språk. Se nedan för detaljer.

Låt oss nu titta på koden.

__init__.py

Först det obligatoriska __init__.py för att definiera insticksmodulens metadata:

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

Det enda anmärkningsvärda funktionen är fältet actual_plugin. Eftersom calibre har både kommandorad och användargränssnitt bör användargränssnittsinsticksmoduler som denna inte läsa in några biblioteksanvändargränssnitt i __init__.py. Fältet actual_plugin gör detta åt dig, genom att berätta för calibre att den egentliga insticksmodulen finns i en annan fil i ditt ZIP-arkiv, som endast kommer att läsas in i ett användargränssnittssammanhang.

Kom ihåg att för att detta ska fungera måste du ha en plugin-import-name-some_name.txt fil i din insticksmoduls ZIP-fil, som diskuterats ovan.

Också finns det ett par olika metoder för att möjliggöra användarkonfiguration av insticksmodulen. Dessa diskuteras nedan.

ui.py

Låt oss nu titta på ui.py som definierar själva användargränssnitts insticksmodul. Källkoden är kraftigt kommenterade och bör vara självförklarande:

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

Den faktiska logiken för att implementera dialogrutan insticksmodulsdemo för gränssnittet.

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'])

Få resurser från insticksmodulens ZIP-fil
calibres insticksmodulsladdningssystem definierar ett par inbyggda funktioner som gör att du bekvämt kan hämta filer från insticksmodulens ZIP-fil.

get_resources(name_or_list_of_names)

Den här funktionen bör anropas med en lista av sökvägar till filer i ZIP-filen. Till exempel för att komma åt filen icon.png i mappbilderna i ZIP-filen, skulle du använda: images/icon.png. Använd alltid ett framåt snedstreck som sökvägsavskiljare, även på Windows. När du anger ett enda namn kommer funktionen att returnera råa byte för filen eller None om namnet inte hittades i ZIP-filen. Om du anger mer än ett namn så returneras en ordbok som mappar namnen till byte. Om ett namn inte hittas kommer det inte att finnas i den returnerade ordboken.

get_icons(name_or_list_of_names, plugin_name=’’)

En wrapper för get_resources() som skapar QIcon-objekt från de råbyte som returneras av get_resources. Om ett namn inte hittas i ZIP-filen kommer motsvarande QIcon att vara null. För att stödja ikontema, skicka in det människovänliga namnet på din insticksmodul som plugin_name. Om användaren använder ett ikontema med ikoner för din insticksmodul, kommer de att läsas in med företräde.

Aktivera användarkonfiguration för din insticksmodul

För att tillåta användare att anpassa din insticksmodul måste du definiera tre metoder i din bas insticksmodulsklass, is_customizable, config_widget och save_settings enligt nedan:

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 har många olika sätt att lagra konfigurationsdata (ett arv av sin långa historia). Det rekommenderade sättet är att använda JSONConfig klass, som lagrar din konfigurationsinformation i en .json-fil.

Koden för att hantera konfigurationsdata i demo-insticksmodulen finns i 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()

prefs-objekt äret nu tillgängligt i hela insticksmodulskoden med en enkel:

from calibre_plugins.interface_demo.config import prefs

Du kan se prefs-objekt som används i main.py:

self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

Redigera bokinsticksmodul

Låt oss nu byta växel för ett tag och se hur man skapar en insticksmodul för att lägga till verktyg till calibres bokredigerare. Instickmodulen finns här: editor_demo_plugin.zip [https://calibre-ebook.com/downloads/editor_demo_plugin.zip].

Det första steget, som för alla insticksmodul är att skapa importnamnet tom TXT-fil, som beskrivs ovan. Vi ska namnge filen plugin-import-name-editor_plugin_demo.txt.

Nu skapar vi den obligatoriska __init __ py filen som innehåller metadata om insticksmodulen – dess namn, utvecklare, version o.s.v.

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)

En enda insticksmodul redigerare kan ge flera verktyg varje verktyg motsvarar en knapp i verktygsfältet och posten i Insticksmoduler-menyn i redigeraren. Dessa kan ha undermenyer om verktyget har flera relaterade åtgärder.

Verktygen bör definieras i filen main.py i din insticksmodul. Varje verktyg är en klass som ärvs från klassen calibre.gui2.tweak_book.plugin.Tool. Låt oss titta på main.py från demo-insticksmodulen, källkoden är tungt kommenterade och bör vara självförklarande. Läs API-dokumenten av klassen calibre.gui2.tweak_book.plugin.Tool för mer detaljer.

main.py

Här kommer vi att se definitionen av ett enda verktyg som kommer att multiplicera alla teckensnittsstorlekar i boken av ett nummer som användaren ger. Detta verktyg visar olika viktiga begrepp som du behöver för att utveckla dina egna insticksmodul, så du bör läsa (tungt kommenterade) källkoden noggrant.

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

Låt oss bryta ner main.py. Vi ser att den definierar ett enda verktyg, som heter Förstora teckensnitt. Detta verktyg kommer att be användaren om ett nummer och multiplicera alla teckensnittsstorlekar i boken med det numret.

Det första viktiga är verktygsnamnet som du måste ställa in till någon relativt unik sträng eftersom det kommer att användas som nyckel för detta verktyg.

Nästa viktiga startpunkt är calibre.gui2.tweak_book.plugin.Tool.create_action(). Denna metod skapar QAction-objekt som visas i insticksmodulsverktygsfältet och insticksmodulsmenyn. Den tilldelar också valfritt en tangentbordsgenväg som användaren kan skräddarsy. Den utlösta signalen från QAction är ansluten till metoden ask_user() som frågar användaren om teckensnittsstorlekensmultiplikatorn och sedan kör förstoringskoden.

Förstoringskoden är väl kommenterad och ganska enkel. De viktigaste sakerna att observera är att du får en referens till redigeringsfönstret som self.gui och redigeraren Boss som self.boss. Boss är objektet som styr redigerarens användargränssnitt. Den har många användbara metoder, som dokumenteras klassen i calibre.gui2.tweak_book.boss.Boss.

Slutligen finns det self.current_container som är en referens till boken som redigeras som calibre.ebooks.oeb.polish.container.Container objekt. Detta representerar boken som en samling av sina konstituerande HTML/CSS/bildfiler och har bekvämliga metoder för att göra många användbara saker. Objektbehållaren och olika användbara nyttofunktioner som kan återanvändas i din insticksmodulskod är dokumenterade i API-dokumentation för e-bokredigeringsverktygen.

Lägga till översättningar till din insticksmodul

Du kan ha alla användargränssnittsträngar i din insticksmodul översatta och visas på vilket språk som är inställt för huvudanvändargränssnittet i calibre.

Det första steget är att gå igenom din insticksmoduls källkod och markera alla användarsynliga strängar som översättningsbara, genom att omge dem _(). Till exempel:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Använd sedan något program för att skapa .po-filer från din insticksmodulskällkod. Det bör finnas en .po-fil för varje språk du vill översätta till. Till exempel: de.po för tyska, fr.po för franska och så vidare. Du kan använda programmet Poedit [https://poedit.net/] för detta.

Skicka dessa .po filer till dina översättare. När du får tillbaka dem, kompilera dem i .mo filer. Du kan återigen använda Poedit för det, eller bara göra:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Placera .mo-filer i translations-mappen i din insticksmodul.

Det sista steget är att helt enkelt anropa funktionen load_translations() överst av dina insticksmoduls .py-filer. Av prestandaskäl bör du bara anropa den här funktionen i de .py-filerna som faktiskt har översättningsbara strängar. Så i en typiskt användargränssnittsinsticksmodul skulle du anropa det överst i ui.py men inte __init __. py.

Du kan testa översättningarna av dina insticksmodul genom att ändra språk i användargränssnittet i calibre i Inställningar → Gränssnitt → Utseende & känsla eller genom att köra calibre med miljövariabeluppsättningen CALIBRE_OVERRIDE_LANG. Till exempel:

CALIBRE_OVERRIDE_LANG=de

Ersätt sv med språkkoden för det språk som du vill testa.

För översättningar med plural, använd funktionen ngettext() istället för _(). Till exempel:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

Insticksmoduls API

Som ni kanske har märkt ovan, en insticksmodul i calibre är en klass. Det finns olika klasser för de olika typerna av insticksmoduler i calibre. Detaljer om varje klass, inklusive basklassen för alla insticksmoduler kan hittas i API-dokumentation för insticksmoduler.

Din insticksmodul kommer nästan säkert att använda kod från calibre. För att lära dig hur man hittar olika funktioner i calibre-kodbasen, läs avsnittet om calibre Kodutformning.

Felsöka insticksmodul

Den första och viktigaste steget är att köra calibre i felsökningsläge. Du kan göra det från kommandoraden med:

calibre-debug -g

Eller inifrån calibre genom att högerklicka på Inställningar eller genom att använda tangentbordsgenvägen Ctrl+Skift+R.

När du kör från kommandoraden, kommer felsökningsutmatning skrivas till konsolen, när man kör inifrån calibre kommer utmatning gå till en TXT-fil.

Du kan infoga utskriftssatser var som helst i din insticksmodulskod, de matas ut i felsökningsläge. Kom ihåg, det här är Python, du borde verkligen inte behöva mer än att skriva ut uttalanden för att felsöka ;) Jag utvecklade hela calibre med just den här felsökningstekniken.

Du kan snabbt testa ändringar i din insticksmodul med hjälp av följande kommandorad:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Detta kommer att stänga en körande calibre, vänta på avstängning är slutförd och sedan uppdatera din insticksmodul i calibre och starta om calibre.

Fler insticksmodul exempel

Du kan hitta en lista med många sofistikerade calibre-insticksmoduler här [https://www.mobileread.com/forums/showthread.php?t=118764].

Dela dina insticksmodul med andra

Om du vill dela insticksmodulen som du har gjort med andra calibre-användare, posta din insticksmodul i en ny tråd i calibre-insticksmodulsforumet [https://www.mobileread.com/forums/forumdisplay.php?f=237].

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- Matematiktypsättning i e-böcker

Matematiktypsättning i e-böcker
calibres e-bokvisare har förmågan att visa matematik inbäddad i e-böcker (EPUB- och HTML-filer). Du kan skriva matematiken direkt med TeX eller MathML eller AsciiMath. calibres e-bokvisare använder det utmärkta biblioteket MathJax [https://www.mathjax.org] för att göra detta. Det här är en kort handledning om hur du skapar e-böcker med matematik i dem som fungerar bra med calibres e-bokvisare.

En enkel HTML-fil med matematik

Du kan skriva matematikindrag inuti i en enkel HTML-fil och calibres e-bokvisare återger den till korrekt inställd matematik. I exemplet nedan använder vi TeX-notering för matematik. Du kommer att se att du kan använda normala TeX-kommandon, med den lilla varningen att et-tecken och mindre än- och större än-tecken måste skrivas som &amp; &lt; och &gt; respektive.

Det första steget är att berätta för calibre att detta kommer att innehålla matematik. Det gör du genom att lägga till följande kodurval i avsnittet <head> av HTML-filen:

<script type="text/x-mathjax-config"></script>

Det var det, nu kan du skriva matematik på samma sätt som i en .tex fil. Till exempel här är Lorentz ekvationer:

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

Detta kodavsnitt liknar följande skärmbild i calibres e-bokvisare.

Lorenz ekvationer

Den kompletta HTML-filen med flera ekvationer och indragsmatematik återges nedan. Du kan konvertera den här HTML filen till EPUB in calibre för att hamna en e-bok som du kan dela ut lätt till andra människor.

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

Mer information

Eftersom calibres e-bokvisare använder MathJax-biblioteket för att återge matematik, är det bästa stället att ta reda på mer om matematik i e-böcker och få hjälp på MathJax-webbplatsen [https://www.mathjax.org].

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- Skapa kataloger för AZW3 • EPUB • MOBI

Skapa kataloger för AZW3 • EPUB • MOBI
calibres Skapa katalogfunktioner kan du skapa en katalog över ditt bibliotek i en mängd olika format. Denna hjälpfil beskriver katalogiseringsalternativ när katalog skapas i AZW3-, EPUB- och MOBI-format.

- Välja böcker att katalogisera

- Inkluderade avsnitt

- Prefix

- Exkluderade böcker

- Exkluderade genrer

- Andra alternativ

- Anpassade katalogomslag

- Ytterligare hjälpresurser

Välja böcker att katalogisera

Om du vill att alla dina bibliotek katalogiseras, ta bort valfri sökning eller filtrera villkor i huvudfönstret. Med en enkelt val bok, kommer alla böcker i ditt bibliotek bli kandidater för inkludering i den skapade katalogen. Individuella böcker kan kommas att exkluderas av diverse orsaker; se Exkluderade genrer sektionen nedan för mer information.

Om du vill bara att några av dina bibliotek katalogiserade, har du två alternativ:

- Skapa ett flertal urval av de böcker du vill katalogisera. Med mer än en bok vald i calibres huvudfönster kommer endast de valda böckerna att katalogiseras.

- Använd sökfält eller taggbläddraren för att filtrera de visade böckerna. Bara de visade böckerna kommer katalogiseras.

För att påbörja kataloggenerering, välj menyalternativet Konvertera böcker > Skapa en katalog över böckerna i ditt calibre-bibliotek. Du kan också lägga till en Skapa katalog-knapp i verktygsfältet i Inställningar > Gränssnitt > Verktygsfält & menyer för att enklare nå dialogrutan för Skapa katalog.

I Katalogalternativ, välj AZW3, EPUB eller MOBI som katalogformatet. I Katalogtitel fältet, ge ett namn för att användas för den skapade katalogen. Om en katalog med samma namn och format redan finns, kommer den att ersättas med den nyligen skapade katalogen.

Aktivering Överför katalog till enhet automatiskt kommer att föra över den skapade katalogen till en ansluten enhet under slutförandet.

Inkluderade avsnitt

Avsnitt aktiverade med en bock kommer att inkluderas i den skapade katalogen:

- Författare - alla böcker, sorterade efter författare, tillgängliga i ett listformat. Icke-serieböcker listas för serieböcker.

- Titlar - alla böcker, sorterade efter titel, tillgänglig i ett listformat.

- Serier - alla böcker som är del av serien, sorterade efter serier, tillgängliga i ett listformat.

- Genrer - individuella genrer, tillgängliga i en lista, sorterade efter författare och serie.

- Nyligen tillagda - alla böcker, sorterade i omvänd kronologisk ordning. Listan innehåller böcker som har lagts till de senaste 30 dagarna, sedan en månad för månad lista över tillagda böcker.

- Beskrivningar - detaljerad beskrivningssida för varje bok, inkluderar omslagsminiatyrbild och kommentarer. Sorterade per författare, med icke-serieböcker listade före serieböcker.

Prefix

Med prefixregler kan du lägga till ett prefix i bokförteckningar när vissa kriterier är uppfyllda. Du kanske till exempel vill markera böcker du har läst med en bock eller böcker på din önskelista med ett X.

Kryssrutan i den första kolumnen aktiverar regeln. Name är ett regelnamn som du anger. Field är antingen Tags eller en egen kolumn från ditt bibliotek. Value är innehållet i Field för att matcha. När en prefixregel är uppfylld, kommer boken vara markerad med vald Prefix.

Tre prefixregler har angivits i exemplet ovan:

1. Läs bok anger att en bok med godtyckligt datum i en anpassad kolumn namngiven Senast läst kommer att prefixas med en bocksymbol.

2. Posten Önskelistprodukt anger att alla böcker med en Önskelistprodukt-tagg kommer att prefixas men en X-symbol.

3. Bibliotek-böcker anger att alla böcker med ett sant (eller Ja) värde i en anpassad kolumn Finns i biblioteket kommer att prefixas med en dubbelpilsymbol.

Den första matchnings regeln tillhandahåller prefixet. Inaktiverad eller ofullständig regel ignoreras.

Exkluderade böcker

Uteslutningsregler tillåter dig specificera böcker som inte ska katalogiseras.

En kryssruta i första kolumnen aktiverar regeln. Name är ett regelnamn du anger. Fält är antingen Tagg eller en anpassad kolumn i ditt bibliotek. Värde är innehållet av Fält att passa mot. När en exkluderingsregel är uppfylld, kommer boken exkluderas från skapad katalog.

Två exkluderingsregler har specificerats i exemplet ovan:

1. Kataloger-regeln anger att alla böcker med en Katalog-tagg kommer att exkluderas från den skapade katalogen.

2. Arkiverad-bokregel anger att alla böcker med ett värde av Arkiverad i en anpassad kolumn Status kommer att exkluderas från den skapade katalogen.

Alla regel utvärderas för varje bok. Inaktiverad eller inkomplett regel ignoreras.

Exkluderade genrer

När katalogen skapas används taggar i din databas som genrer. Till exempel kan du använda taggarna skönlitteratur och facklitteratur. Dessa taggar blir genrer i den skapade katalogen med böcker som anges under respektive genrelista baserat på deras tilldelade taggar. En bok kommer att listas i varje genreavsnitt för vilken den har en motsvarande tagg.

Du kan använda vissa taggar för andra ändamål, kanske ett + för att indikera att en bok lästs, eller en hakparates som [Amazon Freebie] för att indikera en boks källa. Exkluderade genrer-regexet tillåter dig att ange taggar som du inte vill använda som genrer i den skapade katalogen. Standard för exkluderingsregex-mönstret \[.+\]\+ exkluderar alla taggar av formen [tagg], samt exkluderar +, standard tagg för lästa böcker, från att användas som genrer i den skapade katalogen.

Du kan också använda ett exakt namn i regex-uttryck. Till exempel [Amazon Freebie] eller [Project Gutenberg]. Om du vill lista flera exakta taggar för uteslutning, använd en pip-tecken (vertikalt sträck |) mellan dem: [Amazon Freebie]|[Project Gutenberg].

Resultat av regex visar dig vilka taggar som kommer att exkluderas när katalog byggs, baserat på taggar i din databas och regex-mönster som anges. Resultat är uppdateras allt medan du ändrar regex-mönstret.

Andra alternativ

Katalogomslag anger om ett nytt omslag ska skapas eller använda ett befintligt omslag. Det är möjligt att skapa en anpassat omslag för dina kataloger - se custom catalog covers för mer information. Om du har skapat en anpassad omslag som du vill återanvända, väljer du Använd befintligt omslag. Annars, välj Skapa nytt omslag.

Extra beskrivningsanteckning anger innehållet i en anpassad kolumn som ska infogas i beskrivningssidan, bredvid omslagsminiatyrbilden. Du kanske till exempel vill visa datumet du senast läste en bok med hjälp av en anpassad kolumn Senaste lästa. För avancerad användning av beskrivningsanteckningsfunktionen, se det här inlägget i calibre-forumet [https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395].

Miniatyrbildbredd anger breddpreferens för omslagsminiatyrbilder som inkluderas på beskrivningssidor. Miniatyrbilder är cachade för att förbättra prestanda. För att experimentera med olika bredder, försök att skapa en katalog med bara några få böcker tills du har utrönt vilken som är din föredragna bredd och sedan skapa din fullständiga katalog. Första gången en katalog skapas med en ny miniatyrbildbredd kommer prestanda vara långsammare, men efterföljande byggnader av katalogen kommer utnyttja miniatyrbildcachen.

Slå samman med kommentarer anger en anpassad kolumn vars innehåll kommer ovillkorligen slås samman med kommentarmetadata under kataloggenereringen. Du kan till exempel ha en anpassad kolumn Författarbiografi som du vill lägga till i kommentarmetadata. Du kan välja att infoga de anpassade kolumninnehållet före eller efter kommentarsektionen och eventuellt separera det bifogade innehållet med en horisontell regelavskiljare. Godtagbara anpassade kolumntyper inkluderar text, kommentarer och blandningen.

Anpassade katalogomslag

Med insticksmodulen Generate Cover [https://www.mobileread.com/forums/showthread.php?t=124219] installerad kan du skapa anpassade omslag för din katalog. För att installera insticksmodulen, gå till Inställningar > Avancerat > Insticksmoduler > Skaffa nya insticksmoduler.

Ytterligare hjälpresurser

För mer information om calibres katalogfunktion, se MobileRead-forumet Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556], där du kan hitta information för att anpassa katalogmallarna och hur du skickar in en felanmälan.

För att ställa frågor eller diskutera calibres katalogfunktion med andra användare, besök MobileRead-forumet Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236].

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Handledningar »
- Virtuella bibliotek

Virtuella bibliotek

I calibre, är ett virtuellt bibliotek ett sätt att tala om för calibre att öppna en delmängd av ett normalt bibliotek. Till exempel kanske du bara vill arbeta med böcker av en viss författare eller böcker som bara har en viss tagg. Användning av virtuella bibliotek är det föredragna sättet att dela upp en din stora boksamling i mindre delsamlingar. Det är överlägset att dela upp ditt bibliotek i flera mindre bibliotek som, när du vill söka igenom hela samlingen, kan du helt enkelt återvända till hela biblioteket. Det finns inget sätt att söka igenom flera separata bibliotek samtidigt i calibre.

Ett virtuellt bibliotek skiljer sig från en enkel sökning. En sökning kommer bara begränsa listan av visade böcker i boklistan. Ett virtuellt bibliotek kommer göra detta och i tillägg också begränsa posterna som visas i taggbläddraren till vänster. Taggbläddraren visar bara taggar, författare, serier, utgivare o.s.v. som kommer från böckerna i det virtuella biblioteket. Ett virtuellt bibliotek beter sig sålunda som att det faktiska biblioteket bara innehåller den begränsade uppsättningen böcker.

Skapa virtuella bibliotek

För att använda ett virtuellt bibliotek klicka på knappen Virtuellt bibliotek till vänster om sökfältet och välj alternativet Skapa virtuellt bibliotek. Som ett första exempel, låt oss skapa ett virtuellt bibliotek som visar oss bara böckerna av en viss författare. Klicka på länken Författare som visas i bilden nedan och välj den författare du vill använda och klicka på OK.

Dialogrutan Skapa virtuellt bibliotek har fyllts i åt dig. Klicka på OK så ser du att ett nytt Virtuellt bibliotek har skapats och byts automatiskt till, som bara visar böckerna av den valda författaren. När det gäller calibre är det som om ditt bibliotek endast innehåller böckerna av den valda författaren.

Du kan när som helst växla tillbaka till hela biblioteket genom att klicka igen på Virtuellt bibliotek och välja posten med namnet <None>.

Virtuella bibliotek baseras på sökningar. Du kan använda godtycklig sökning som grund av ett virtuellt bibliotek. Det virtuella biblioteket kommer att innehålla bara böcker som matchar denna sökning. Skriv först in den sökning du vill använda i sökfältet eller bygg en sökning med taggbläddraren. Klicka på knappen Virtuellt bibliotek när du är nöjd med resultatet, välj Skapa bibliotek och mata in ett namn på det nya virtuella biblioteket. Det virtuella biblioteket kommer då skapas baserat på sökbegreppet du just matat in. Sökningar är mycket kraftfulla, för exempel på vad du kan göra med dem, se Sökgränssnittet.

Exempel på användbara virtuella bibliotek

- Böcker tillagda i calibre den senaste dagen:

date:>1daysago

- Böcker tillagda i calibre den senaste månaden:

date:>30daysago

- Böcker med betyg på 5 stjärnor:

rating:5

- Böcker med betyg på minst 4 stjärnor:

rating:>=4

- Böcker utan betyg:

rating:false

- Tidskrifter hämtade med funktionen Hämta nyheter i calibre:

tags:=nyheter and author:=calibre

- Böcker utan taggar:

tags:false

- Böcker utan omslag:

cover:false

Arbeta med virtuella bibliotek

Du kan redigera tidigare skapat virtuellt bibliotek eller ta bort det genom att klicka på Virtuellt bibliotek och välja lämplig åtgärd.

Du kan tala om för calibre att du alltid vill tillämpa ett visst virtuellt bibliotek när det aktuella biblioteket öppnas genom att gå till Inställningar → Gränssnitt → Beteende.

Du kan snabbt använda den aktuella sökningen som ett temporärt virtuellt bibliotek genom att klicka på knappen Virtuellt bibliotek och välja posten *aktuell sökning.

Du kan visa alla tillgängliga virtuella bibliotek som flikar ovanför boklistan. Det är speciellt praktiskt om du vill växla mellan virtuella bibliotek mycket ofta. Klicka på knappen Virtuellt bibliotek och välj Visa virtuella bibliotek som flikar. Du kan ordna om flikarna genom att dra och släppa och stänga dem du inte vill se. Stängda flikar kan återställas genom högerklicka på flikraden.

Använda virtuella bibliotek i sökningar

Du kan söka efter böcker som finns i ett virtuellt bibliotek med prefixet vl:. Till exempel hittar vl:Lästa all böckerna i det virtuella biblioteket Lästa. Sökningen vl:Lästa and vl:"Science Fiction" hittar alla böckerna som finns i både de virtuella biblioteken Lästa och Science Fiction.

Värdet efter vl: måste vara namnet på ett virtuellt bibliotek. Om det virtuella bibliotekets namn innehåller blanksteg, omge det med citattecken.

En användning för en sökning i virtuella bibliotek är i innehållsservern. I Inställningar → Dela över nätet → Kräv användarnamn och lösenord kan du begränsa vilka calibre-bibliotek som är synliga för en användare. För varje synligt bibliotek kan du ange ett sökuttryck för att ytterligare begränsa vilka böcker som visas. Använd vl:"Namn på virtuellt bibliotek" för att begränsa böckerna till dessa i ett virtuellt bibliotek.

Använda ytterligare begränsningar

Du kan ytterligare begränsa böckerna som visas i ett virtuellt bibliotek genom att använda Ytterligare begränsningar. En ytterligare begränsning är en Sparad sökning du tidigare skapat som kan tillämpas på det aktuella virtuella biblioteket för att ytterligare begränsa böckerna som visas i ett virtuellt bibliotek. Säg till exempel att du har ett virtuellt bibliotek för böcker markerade som historisk skönlitteratur och en Sparad sökning som visar olästa böcker, du kan klicka på knappen Virtuellt bibliotek och välja alternativet Ytterligare begränsningar för att endast visa olästa historiska skönlitteraturböcker. För att lära dig om sparade sökningar, se Spara sökningar.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- calibre:// URL-schema

calibre:// URL-schema
calibre registrerar sig som hanterarprogram för calibre:// URL:er. Så du kan använda dessa för att utföra åtgärder som att öppna böcker, söka efter böcker osv. från andra program/dokument eller via kommandoraden. Till exempel, kör följande på kommandoraden:

calibre calibre://switch-library/Some_Library

Kommer att öppna calibre med biblioteket med namnet Some Library. Biblioteksnamn är mappnamnet på biblioteksmappen med blanksteg ersatta med understrykningar. Specialvärdet _ betyder det aktuella biblioteket. De olika typerna av URL:er dokumenteras nedan.

Du kan till och med placera dessa länkar inuti HTML-filer eller Word-dokument eller liknande och operativsystemet kommer automatiskt att köra calibre för att utföra den angivna åtgärden.

- Byt till ett specifikt bibliotek

- Visa en specifik bok i calibre

- Öppna en specifik bok i e-bokvisaren på en specifik position

- Söker efter böcker

- Öppna ett bokdetaljfönster på en bok i något bibliotek

- Öppna anteckningarna som är kopplade till en författare/serie/osv.

- Hexkodning av URL-parametrar

Byt till ett specifikt bibliotek

URL-syntaxen är:

calibre://switch-library/Library_Name

Biblioteksnamn är bibliotekets mappnamn med blanksteg ersatta med understrykningar. Specialvärdet _ betyder det aktuella biblioteket. Du kan också använda hexkodning för biblioteksnamnen, användbart om biblioteksnamnen har specialtecken som annars skulle kräva URL-kodning. Hexkodade biblioteksnamn ser ut som:

_hex_-AD23F4BC

Där delen efter prefixet _hex_- är biblioteksnamnet kodat som UTF-8 och varje byte representerad av två hexadecimala tecken.

Visa en specifik bok i calibre

URL-syntaxen är:

calibre://show-book/Library_Name/book_id

Detta visar boken med book_id (ett nummer) i calibre. Id för böcker kan ses i calibres gränssnitt genom att föra muspekaren över länken Klicka för att öppna i :guilabel:`bokdetalj`panelen, det är numret inom parentes i slutet av sökvägen till bokmappen.

Du kan kopiera en länk till den aktuella boken som visas i calibre genom att högerklicka på: bokdetalj`panelen och välja :guilabel:`Kopiera länk till bok.

Om en sökning är aktiv och boken inte matchas av sökningen rensas sökningen.

Om ett virtuellt bibliotek väljs kommer calibre att använda det när boken visas. Om boken inte finns i det virtuella biblioteket rensas det virtuella biblioteket.

Om du vill byta till ett visst virtuellt bibliotek när du visar boken, använd:

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

ersätter mellanslag i det virtuella bibliotekets namn med %20. Om boken inte finns i det virtuella biblioteket ignoreras det virtuella biblioteket.

Öppna en specifik bok i e-bokvisaren på en specifik position

URL-syntaxen är:

calibre://view-book/Library_Name/book_id/book_format?open_at=location

Här, book_format är bokens format, till exempel EPUB eller MOBI och location är en valfri plats inuti boken. Det enklaste sättet att få dessa länkar är att öppna en bok i visaren och välj sedan i visningskontrollerna Gå till → Plats och där kommer en sådan länk att ges som du kan kopiera/klistra in någon annanstans.

Söker efter böcker

URL-syntaxen är:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

Här är förfrågan valfritt sökuttryck. Om sökuttrycket är komplicerat, koda det som en hexsträng och använd istället eq. Att utesluta förfrågan kommer att göra att den aktuella sökningen rensas.

Som standard, om ett virtuellt bibliotek är valt, rensar calibre det innan du gör sökningen för att säkerställa att alla böcker hittas. Om du vill bevara det virtuella biblioteket, använd:

calibre://search/Library_Name?q=query&virtual_library=_

Om du vill växla till ett särskilt virtuellt bibliotek, använd:

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

ersätta blanksteg i det virtuella biblioteksnamnet med %20.

Om du utför en sökning i calibre och vill skapa en länk för den kan du göra det genom att högerklicka på sökfältet och välja Kopiera sökning som URL.

Öppna ett bokdetaljfönster på en bok i något bibliotek

URL-syntaxen är:

calibre://book-details/Library_Name/book_id

Det här öppnar ett fönster med bokdetaljer för den angivna boken från det angivna biblioteket utan att ändra det aktuella biblioteket eller den valda boken.

Öppna anteckningarna som är kopplade till en författare/serie/osv.

URL-syntaxen är:

calibre://book-details/Library_Name/Field_Name/id_Item_Id

Detta öppnar ett fönster som visar anteckningarna för det angivna objektet. Det enklaste sättet att skapa sådana webbadresser är att visa de anteckningar du vill ha i calibre och klicka på knappen Kopiera URL för att kopiera URL:en till urklipp och klistra in den där du behöver.

Här är Fält_namn namnet på kolumnerna som authors eller tags. För användarskapade kolumner, ersätt det inledande # i fältnamnet med ett understreck, så #minataggar blir _minataggar.

Förutom att specificera objekt med id med Item_Id kan du också specificera dem med namn med antingen val_Item_Name eller hex_Hex_Encoded_Item_Name. Till exempel:

calibre://book-details/Library_Name/authors/val_John%20Doe

Hexkodning av URL-parametrar

Hexkodning av URL-parametrar görs genom att först koda parametern som UTF-8 byte och sedan ersätta varje byte med två hexadecimala tecken som representerar byten. Till exempel är strängen abc är byten 0x61 0x62 och 0x63 i UTF-8, så den kodade versionen är strängen: 616263.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Anpassa calibre

Anpassa calibre
calibre har en mycket modulär design. Olika delar av den kan anpassas. Här får du lära dig:

- hur man använder miljövariabler och justeringar för att anpassa calibres beteende,

- hur du anger dina egna statiska resurser som ikoner och mallar för att åsidosätta standardinställningarna

- hur man använder insticksmoduler för att lägga till funktionalitet till calibre.

- hur man delar ikonteman och insticksmoduler med andra calibre-användare.

- för att se hur man skapar recept för att lägga till nya källor av innehåll på nätet till calibre, besök avsnittet Lägga till din favorit nyhetswebbplats.

Observera

Observera att även om ikonteman och insticksmoduler är indexerade och hämtningsbara via calibres inbyggda uppdateringsprogram, är de inte en del av calibre, och deras kanoniska platser för support och källkod finns på Mobileread-forumen [https://www.mobileread.com/forums/forumdisplay.php?f=166] i deras supporttrådar.

- Miljövariabler

- Justeringar

- Åsidosätta ikoner, mallar o.s.v.

- Skapa dina egna ikonteman för calibre

- Anpassa calibre med insticksmoduler

Miljövariabler

- CALIBRE_CONFIG_DIRECTORY - ställer in mappen var konfigurationsfiler lagras/läses.

- CALIBRE_TEMP_DIR - ställer in den temporära mappen som används av calibre

- CALIBRE_CACHE_DIRECTORY - ställer in mappen calibre använder för att cacha beständiga data mellan sessioner

- CALIBRE_OVERRIDE_DATABASE_PATH - tillåter dig att ange fulla sökvägen till metadata.db. Genom att använda denna variabel kan du ha metadata.db i en annan plats än biblioteksmappen. Användbart om din biblioteksmapp är på ett nätverk som inte stöder låsning av filer.

- CALIBRE_DEVELOP_FROM - används för att köra från en calibre utvecklingsmijö. Se Inrätta en calibre-utvecklingsmiljö.

- CALIBRE_OVERRIDE_LANG - används för att tvinga användning av språket som används i gränssnittet (ISO 639-språkkod)

- CALIBRE_TEST_TRANSLATION - används för att testa en översättnings .po-fil (bör vara sökvägen till .po-filen)

- CALIBRE_NO_NATIVE_FILEDIALOGS - får calibre att inte använda inbyggda fildialogrutor för att välja filer/mappar.

- CALIBRE_NO_NATIVE_MENUBAR - gör att calibre inte skapar en inbyggd (global) meny på Ubuntu Unity och liknande Linux-skrivbordsmiljöer. Menyn placeras istället inuti fönstret, som är traditionellt.

- CALIBRE_USE_SYSTEM_THEME - som standard använder calibre sin egen inbyggda Qt-stil på Linux. Detta för att undvika kraschar och hängningar orsakade av inkompatibiliteter mellan versionen av Qt-calibre är byggd mot och systemets Qt. Nackdelen är att calibre kanske inte följer systemets utseende och känsla. Om du ställer in denna miljövariabel på Linux kommer calibre att använder systemtemat – se upp för kraschar och hängningar.

- CALIBRE_SHOW_DEPRECATION_WARNINGS - får calibre att skriva ut deprecieringsvarningar till stdout. Användbart för calibre utvecklare.

- CALIBRE_NO_DEFAULT_PROGRAMS - förhindra calibre från att automatiskt registrera de filtyper den är kapabel att hantera med Windows.

- QT_QPA_PLATFORM - Ställ in detta till wayland på Linux för att tvinga calibre att använda Wayland och xcb för att tvinga användning av X11.

- SYSFS_PATH - Använd om sysfs är monterad någon annanstans än /sys

- http_proxy, https_proxy - används på Linux för att ange en HTTP(S)-proxy

Se Hur man ställer in miljövariabler i Windows [https://www.computerhope.com/issues/ch000549.htm]. Om du använder macOS kan du ställa in miljövariabler genom att skapa ~/Library/Preferences/calibre/macos-env.txt och ställa in miljövariablerna en per rad i den, till exempel:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Justeringar

Justeringar är små ändringar som du kan specificera för att kontrollera olika aspekter av calibres beteende. Du kan ändra dem genom att gå till Inställningar->Avancerat->Justeringar. Standardvärdena för justeringarna återges nedan

#!/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'

Åsidosätta ikoner, mallar o.s.v.

Observera
calibre har direkt stöd för ikonteman, det finns flera ikonteman tillgängliga för calibre som du kan använda genom att gå till Inställningar → Gränssnitt → Utseende & känsla → Ändra ikontema. Det är att föredra att använda ikonteman framför åsidosättande enskilda ikoner.

calibre låter dig åsidosätta de statiska resurserna, som ikoner, JavaScript och mallar för metadataomslag, kataloger o.s.v. med dina anpassade versioner. Alla statiska resurser lagras i undermappen för resurser på installationsplatsen för calibre. På Windows är detta vanligtvis C:\Program Files\Calibre2\resources. På macOS, /Applications/calibre.app/Contents/Resources/resources/. På Linux, om du använder det binära installationsprogrammet från calibres webbplats blir det /opt/calibre/resources. Dessa sökvägar kan ändras beroende på var du väljer att installera calibre.

Du ska inte ändra filer i denna resursmapp, eftersom dina ändringar kommer skrivas över nästa gång du uppdaterar calibre. Gå istället till Inställningar → Avancerat → Diverse och klicka Öppna konfigurationsmappen för calibre. I denna konfigurationsmapp skapar du en undermapp med namnet resurser och placerar filerna du vill åsidosätta i den. Placera filer i lämplig undermapp, som till exempel för bilder resources/images o.s.v. calibre kommer automatisk använda dessa anpassade inställningarna istället för de inbyggda nästa gång det startar.

Till exempel om du vill använda ikonen för åtgärden Ta bort böcker, ska du för titta i den inbyggda resursmappen och se att den relevanta filen är resources/images/remove_books.png. Med antagandet att du har en alternativ ikon i PNG-format kallad my_remove_books.png kommer du spara den i konfigurationsmappen som resources/images/remove_books.png. Alla ikoner som används av calibres användargränssnitt är i resources/images och dess undermappar. Att placera en åsidosättande fil här kommer att ha ännu högre prioritet än ett anpassat ikontema.

Skapa dina egna ikonteman för calibre

If you have created a beautiful set of icons and wish to share them with other calibre users via calibre’s builtin icon theme support, you can easily package up your icons into a theme. To do so, go to Preferences → Miscellaneous → Create icon theme, select the folder where you have put your icons. Then fill up the theme metadata and click OK. This will result in a ZIP file containing the theme icons. You can upload that to the calibre forum at Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] and then I will make your theme available via calibre’s builtin icon theme system.
By default, the icon theme you just created will also be installed as the current theme in calibre. If you are testing your theme, remember to remove the images from the resources/images folder so that the icons from the theme are used.

Från och med calibre 6 kan du ha anpassade ikoner för ljust och mörkt läge. Skapa helt enkelt två versioner av ikonen och namnge filerna med suffixet -for-dark-theme och -for-light-theme. Till exempel, modified-for-dark-theme.png och modified-for-light-theme.png. Då kommer calibre automatiskt att använda lämplig ikon baserat på det aktuella temat.

Anpassa calibre med insticksmoduler
calibre är en väldigt modulär konstruktion. Nästan alla funktioner i calibre kommer i form av insticksmoduler. Insticksmoduler används för konvertering, för hämtning av nyheter (även om dessa kallas för recept), för diverse komponenter av användargränssnitt, för anslutning av olika enheter, för bearbetning av filer när de läggs till calibre och så vidare. Du kan få en komplett lista med alla inbyggda insticksmoduler i calibre genom att gå till Inställningar → Avancerat → Insticksmoduler.

Du kan skriva dina egna insticksmoduler för att anpassa och utöka beteendet av calibre. Insticksmodulsarkiteturen i calibre är mycket enkel, se handledningen Skriv dina egna insticksmoduler för att utöka calibres funktionalitet.

När du har skrivit en insticksmodul kan du ladda upp den till insticksmodulsforumet för calibre på Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237] och den kommer att göras tillgängligt via calibres inbyggda insticksmodulsuppdaterare.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Anpassa calibre »
- API-dokumentation för insticksmoduler

API-dokumentation för insticksmoduler

Definiera diverse abstrakta grundklasser som kan vara underklasser för att skapa kraftfulla insticksmoduler. Dessa användbara klasser är:

- Insticksmoduler

- FileTypePlugin

- Metadatainsticksmodul

- Katalog insticksmodul

- Insticksmoduler för metadatahämtning

- Konvertering insticksmodul

- Enhetsdrivrutiner

- Användargränssnittsåtgärder

- Inställningar insticksmodul

Insticksmoduler
class calibre.customize.Plugin(plugin_path)[source]

En calibre insticksmodul. Användbara medlemmar inkluderar:

- self.installation_type: Lagrar hur insticksmoudlen installerades.

- self.plugin_path: Lagrar sökväg till ZIP-filen som innehåller

denna insticksmodul eller None om den är en inbyggd insticksmodul

- self.site_customization: Lagrar en anpassningssträng som har angetts

av användaren.

Metoder som ska åsidosättas i underklasser:

- initialize()

- customization_help()

Användbara metoder:

- temporary_file()

- __enter__()

- load_resources()

supported_platforms = []

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

Namnet på denna insticksmodul. Du måste ange något annan än Trivial Plugin för att den ska fungera.

version = (1, 0, 0)

Versionen för denna insticksmodul är n 3-tupel (major, minor, revision)

description = 'Gör absolut ingenting'

En kort sträng som beskriver vad denna insticksmodul gör

author = 'Okänd'

Författare av denna insticksmodul

priority = 1

När mer än en insticksmodul finns för en filtyp, kommer insticksmodulen att köras i fallande prioritet. Insticksmoduler med högre prioritet kommer att köras först. Det högsta möjliga prioriten är sys.maxsize. Standard prioritet är 1.

minimum_calibre_version = (0, 4, 118)

Den tidigaste versionen av calibre som denna insticksmodul kräver

installation_type = None

Sättet den här insticksmodulen installeras

can_be_disabled = True

Om False, kommer inte användaren kunna deaktivera denna insticksmodul. Använd med försiktighet.

type = 'Grund'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

initialize()[source]

Anropas när calibre-insticksmoduler initieras. Insticksmoduler är återinitierade varje gång en ny insticksmodul läggs till. Observera också att om insticksmodulen körs i en arbetsprocess, såsom att lägga till böcker, då kommer insticksmodulen att initieras för varje ny arbetsprocess.

Utför godtyckligt insticksmodul initialisering här, såsom att utvinna resurser från insticksmoduls ZIP-filen. Sökvägen till ZIP-filen är tillgänglig som self.plugin_path.

Observera att self.site_customization är inte tillgänglig vid detta tillfälle.

config_widget()[source]

Implementera denna metod och save_settings() i din insticksmodul för att använda en anpassad konfigurationsdialog, snarare än att förlita sig på en enkel strängbaserad standardanpassning.

Den här metoden, om den implementeras, måste returnera en QWidget. Gränssnittskomponenten kan ha en alternativ metod validate() som inte tar några argument och anropas omedelbart efter användaren klickar OK. Ändringar tillämpas om och endast om metoden returnerar True.

Om du av någon anledning inte kan utföra konfigurationen vid denna tid, returnera en tupel av två strängar (meddelande, detaljer), kommer dessa att visas som en varningsdialogruta för användaren och processen avbryts.

save_settings(config_widget)[source]

Spara inställningarna som anges av användaren med config_widget.

Parametrar:

config_widget – Gränssnittskomponenten som returneras av config_widget().

do_user_config(parent=None)[source]

Denna metod visar en inställningsdialogruta för denna insticksmodul. Den returnerar True om användaren klickar på OK, annars False. Ändringarna tillämpas automatiskt.

load_resources(names)[source]

Om denna insticksmodul kommer i en ZIP-fil (användartillagd insticksmodul), kommer denna metod tillåta dig läsa in resurser från ZIP-fil.

Till exempel att läsa in en bild:

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

Parametrar:

names – Förteckning över sökvägar till resurser i ZIP-filen som använder / som avskiljare

Returnerar:

En ordbok av formen {name:file_contents}. Namn som inte finns i ZIP-filen kommer inte att vara närvarande i ordboken.

customization_help(gui=False)[source]

Returnera en sträng som ger hjälp om hur du anpassar denna insticksmodul. Som standard upp en NotImplementedError, vilket tyder på att insticksmodulen inte kräver anpassning.

Om du återimplementerar den här metoden i din underklass, kommer användaren att bli ombedd att skriva in en sträng som anpassning för denna insticksmodul. Den anpassning strängen kommer att finnas tillgänglig som self.site_customization.

Platsanpassning skulle kunna vara vad som helst, till exempel vägen till en välbehövlig binär på användarens dator.

Parametrar:

gui – Om True returneras HTML-hjälp, annars returneras oformaterad hjälptext.

temporary_file(suffix)[source]

Returnera en fil-liknande objekt som är en temporär fil i filsystemet. Denna fil kommer att vara tillgänglig även efter nedstängning och kommer endast att tas bort på vid avstängning. Använd name medlem av den återgivna objektet för att komma åt den fullständiga sökvägen till den skapade temporärfilen.

Parametrar:

suffix – Suffixet att den temporära filen kommer att ha.

cli_main(args)[source]

Denna metod är den huvudsakliga ingångspunkten för dina insticksmodulers kommandoradsgränssnitt. Det anropas när användaren utför: calibre-debug -r ”insticksmodulsnamn”. Eventuella argument som skickas finns i variabeln args.

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[source]

Grunder: Plugin

En insticksmodul som är associerad med en viss uppsättning filtyper.

file_types = {}

Uppsättning av filtyper som denna insticksmodul ska köra. Använd ”*” för alla filtyper. Till exempel: {'lit', 'mobi', 'prc'}

on_import = False

Om True, körs denna insticksmodul när böcker läggs till i databasen

on_postimport = False

Om True, körs denna insticksmodul efter att böcker läggs till i databasen. I detta fall anropas metoderna postimport och postadd av insticksmodulen.

on_postconvert = False

Om True, körs denna insticksmodul efter att en bok har konverterats. I det här fallet anropas insticksmodulens postkonverteringsmetod.

on_postdelete = False

Om True, körs denna insticksmodul efter att en bokfil har tagits bort från databasen. I det här fallet anropas insticksmodulens postborttagningsmetod.

on_preprocess = False

Om True, körs denna insticksmodul alldeles innan en konvertering

on_postprocess = False

Om True, körs denna insticksmodul efter konvertering på den slutliga filen som produceras av insticksmodulen för konverteringsutmatning.

type = 'Filtyp'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

run(path_to_ebook)[source]

Kör insticksmodulen. Behöver implementeras i underklasser. Det ska utföra vilka som helst modifikationer som krävs på e-boken och returnera den absoluta sökvägen till den modifierade e-boken. Om inga modifikationer behövs, bör sökvägen återgå till den ursprungliga e-boken. Om ett fel uppstår, ska det framkalla ett undantag. Standard implementationen helt enkelt returnerar sökvägen till den ursprungliga e-boken. Observera att sökvägen till originalfilen (innan några filtyp insticksmodul körs, finns som self.original_path_to_file).

Den modifierade e-bokfilen ska skapas med temporary_file() metod.

Parametrar:

path_to_ebook – Absolut sökväg till e-boken.

Returnerar:

Absolut sökväg till den modifierade e-boken.

postimport(book_id, book_format, db)[source]

Anropas efter importering, dvs. efter bokfilen har lagts till i databasen. Observera att detta skiljer sig från postadd() som anropas när bokposten skapas för första gången. Denna metod anropas varje gång en ny fil läggs till i en bokpost. Det är användbart för att ändra bokposten baserat på innehållet i den nyligen tillagda filen.

Parametrar:

- book_id – Databas id för tillagda boken.

- book_format – Filtypen för boken som lades till.

- db – Biblioteksdatabas.

postconvert(book_id, book_format, db)[source]

Anropas efter konvertering, dvs. efter att konverteringsutmatningsboken har lagts till i databasen. Observera att den körs endast efter en konvertering, inte efter att en bok har lagts till. Det är användbart för att ändra bokposten baserat på innehållet i den nyligen tillagda filen.

Parametrar:

- book_id – Databas id för tillagda boken.

- book_format – Filtypen för boken som lades till.

- db – Biblioteksdatabas.

postdelete(book_id, book_format, db)[source]

Anropas postborttagning, dvs. efter att bokfilen har tagits bort från databasen. Observera att den inte körs när en bokpost tas bort, endast när ett eller flera format från boken tas bort. Det är användbart för att ändra bokposten baserat på formatet på den borttagna filen.

Parametrar:

- book_id – Databas id för tillagda boken.

- book_format – Filtypen för boken som lades till.

- db – Biblioteksdatabas.

postadd(book_id, fmt_map, db)[source]

Anropad efter posttilläggning, dvs. efter en bok har lagts till i databasen. Observera att detta skiljer sig från postimport() som anropas efter en enskild bokfil har lagts till en bok. postadd() anropas när en hel bokpost med mer än en möjlig bokfil har skapats för första gången. Det är användbart för att ändra bokposten i databasen när boken läggs till för första gången till calibre.

Parametrar:

- book_id – Databas id för tillagda boken.

- fmt_map – Lista över filformat till sökväg från vilken filformatet lades till. Observera att detta kan eller kanske inte hänvisar till en faktisk befintlig fil, eftersom filer ibland läggs till som strömmar. I så fall kan det vara ett skenvärde eller en obefintlig sökväg.

- db – Biblioteksdatabas

Metadatainsticksmodul

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[source]

Grunder: Plugin

En insticksmodul som implementerar läsning av metadata från en uppsättning filtyper.

file_types = {}

Uppsättning med filtyper som denna insticksmodul ska köra. Till exempel: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

version = (7, 10, 0)

Versionen för denna insticksmodul är n 3-tupel (major, minor, revision)

author = 'Kovid Goyal'

Författare av denna insticksmodul

type = 'Metadataläsare'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

get_metadata(stream, type)[source]

Returnera metadata för filen som representeras av ström (ett filliknande objekt som stöder läsning). Markera ett undantag när det finns ett fel med inmatningen.

Parametrar:

type – Filtypen. Garanterat en av posterna i file_types.

Returnerar:

Ett calibre.ebooks.metadata.book.Metadata objekt

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[source]

Grunder: Plugin

En insticksmodul som implementerar läsning av metadata från en uppsättning filtyper.

file_types = {}

Uppsättning med filtyper som denna insticksmodul ska köra. Till exempel: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

version = (7, 10, 0)

Versionen för denna insticksmodul är n 3-tupel (major, minor, revision)

author = 'Kovid Goyal'

Författare av denna insticksmodul

type = 'Metadataskrivare'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

set_metadata(stream, mi, type)[source]

Ställ metadata för filen som representeras av ström (en fil som objekt som stöder läsning). Markerar ett undantag när det finns ett fel med inmatning.

Parametrar:

- type – Filtypen. Garanterat en av posterna i file_types.

- mi – Ett calibre.ebooks.metadata.book.Metadata objekt

Katalog insticksmodul

class calibre.customize.CatalogPlugin(plugin_path)[source]

Grunder: Plugin

En insticksmodul som implementerar en kataloggenerator.

file_types = {}

Utmatningsfiltyp som denna insticksmodul ska köra. Till exempel: ’epub’ eller ’xml’

type = 'Kataloggenerator'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

cli_options = []

CLI-parseralternativ som är specifika för denna insticksmodul deklareras som namedtuple Option:

från uppsättningar importera namnsatta tupler 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 analyseras i calibre.db.cli.cmd_catalog:option_parser()

initialize()[source]

Om insticksmodulen inte är ett inbyggt, kopiera insticksmodulens .ui- och .py-filer från ZIP-filen till $TMPDIR. Flik kommer att skapas dynamiskt och läggas till dialogrutan Katalogalternativ i calibre.gui2.dialogs.catalog.py:Catalog

run(path_to_output, opts, db, ids, notification=None)[source]

Kör insticksmodulen. Måste implementeras i underklasser. Det ska skapa katalogen i det format som anges i file_types, återvänder den absoluta sökvägen till den skapade katalogfilen. Om ett fel inträffar bör det signalera ett undantag.

Den skapade katalogfilen ska skapas med temporary_file()-metoden.

Parametrar:

- path_to_output – Absoluta sökvägen till den skapade katalogfilen.

- opts – En ordbok av nyckelordsargument

- db – Ett LibraryDatabase2-objekt

Insticksmoduler för metadatahämtning
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[source]

Grunder: Plugin

type = 'Metadatakälla'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

author = 'Kovid Goyal'

Författare av denna insticksmodul

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

capabilities = frozenset({})

Uppsättning funktioner som stöds av denna insticksmodul. Användbara funktioner är: ”identify”, ”cover”

touched_fields = frozenset({})

Lista med metadatafält som potentiellt kan hämtas av denna insticksmodul under identifieringsfasen

has_html_comments = False

Ange detta som True om din insticksmodul returnerar HTML-formaterade kommentarer

supports_gzip_transfer_encoding = False

Om detta ställs till True betyder det att webbläsarobjektet indikerar att det stöder gzip-överföringskodning. Detta kan hämtas snabbt, men se till att källan faktiskt stöder gzip-överföringskodningen korrekt först

ignore_ssl_errors = False

Ställ in detta till True för att ignorera HTTPS-certifikatfel när du ansluter till den här källan.

cached_cover_url_is_reliable = True

Cachade omslags-URL:er kan ibland vara opålitliga (dvs. hämtningen kan misslyckas eller den återgivna bilden kan vara falsk). Om så ofta är fallet med den här källan, ställ in den till False

options = ()

En lista med Alternativ objekt. De kommer användas för automatiskt skapa konfigurationsgränssnittskomponenten för denna insticksmodul

config_help_message = None

En sträng som visas högst upp på gränssnittskomponenten för denna insticksmodul

can_get_multiple_covers = False

Om True kan denna källa returnera flera omslag för en viss förfråga

auto_trim_covers = False

Om inställt till True beskärs hämtade omslag automatiskt med denna insticksmodul.

prefer_results_with_isbn = True

Om värdet är True, och denna källa returnerar flera resultat för en förfråga, av vilka några har ISBN och av vilka en del inte, så ignoreras resultaten utan ISBN

is_configured()[source]

Returnera False om din insticksmodul måste anpassas innan den kan användas. Till exempel kan den behöva ett användarnamn/lösenord/API-nyckel.

customization_help()[source]

Returnera en sträng som ger hjälp om hur du anpassar denna insticksmodul. Som standard upp en NotImplementedError, vilket tyder på att insticksmodulen inte kräver anpassning.

Om du återimplementerar den här metoden i din underklass, kommer användaren att bli ombedd att skriva in en sträng som anpassning för denna insticksmodul. Den anpassning strängen kommer att finnas tillgänglig som self.site_customization.

Platsanpassning skulle kunna vara vad som helst, till exempel vägen till en välbehövlig binär på användarens dator.

Parametrar:

gui – Om True returneras HTML-hjälp, annars returneras oformaterad hjälptext.

config_widget()[source]

Implementera denna metod och save_settings() i din insticksmodul för att använda en anpassad konfigurationsdialog, snarare än att förlita sig på en enkel strängbaserad standardanpassning.

Den här metoden, om den implementeras, måste returnera en QWidget. Gränssnittskomponenten kan ha en alternativ metod validate() som inte tar några argument och anropas omedelbart efter användaren klickar OK. Ändringar tillämpas om och endast om metoden returnerar True.

Om du av någon anledning inte kan utföra konfigurationen vid denna tid, returnera en tupel av två strängar (meddelande, detaljer), kommer dessa att visas som en varningsdialogruta för användaren och processen avbryts.

save_settings(config_widget)[source]

Spara inställningarna som anges av användaren med config_widget.

Parametrar:

config_widget – Gränssnittskomponenten som returneras av config_widget().

get_author_tokens(authors, only_first_author=True)[source]

Ta en lista över författare och returnera en lista med tokens som är användbara för en AND-sökförfråga. Denna funktion försöker returnera tokens i ordningen förnamn mellannamn efternamn, genom att anta att om ett kommatecken finns i författarnamnet, är namnet i efternamn, andra namnbildningar.

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[source]

Ta en titel och returnera en lista med symboler som är användbara för en OCH sökförfrågan. Exkluderar connectives (valfritt) och skiljetecken.

split_jobs(jobs, num)[source]

Dela upp en lista med jobb i högst num grupper, så jämnt som möjligt

test_fields(mi)[source]

Återgå till det första fältet från self.touched_fields som är null på mi objektet

clean_downloaded_metadata(mi)[source]

Anropa den här metoden i din insticksmodul för att identifiera metod att normalisera metadata innan metadataobjektet läggs i result_queue. Du kan naturligtvis använda en anpassad algoritm som matchar din metadatakälla.

get_book_url(identifiers)[source]

Returnera en 3-tupel eller None. 3-tupeln har formen: (identifier_type, identifier_value, URL). URL:en är webbadressen till boken som identifierats av identifierare vid denna källa. identifier_type, identifier_value anger identifieraren som motsvarar URL:en. Den här webbadressen måste vara tillgänglig för en person med en webbläsare. Den är avsedd att tillhandahålla en klickbar länk för användaren att enkelt besöka bokens sida vid denna källa. Om ingen webbadress hittas, returneras None. Denna metod måste vara snabb och konsekvent, så implementera den bara om det är möjligt att konstruera webbadressen från ett känt schema med identifierare.

get_book_url_name(idtype, idval, url)[source]

Returnera ett mänskligt läsbart namn från det återgivna värdet för get_book_url().

get_book_urls(identifiers)[source]

Åsidosätt den här metoden om du vill returnera flera URL:er för den här boken. Returnera en lista med 3-tupler. Som standard anropar den här metoden helt enkelt get_book_url().

get_cached_cover_url(identifiers)[source]

Returnera cachade omslags-URL:er för boken som identifieras av identifierarordboken eller None om det inte finns någon sådan URL.

Observera att den här metoden endast får returnera validerade URL:er, dvs. inte URL:er som kan resultera i en generisk omslagsbild eller ett fel som inte hittades.

id_from_url(url)[source]

Analysera en URL och returnera en tupel av formen: (identifier_type, identifier_value). Om URL:en inte matchar mönstret för metadatakällan, returnera None.

identify_results_keygen(title=None, authors=None, identifiers={})[source]

Returnera en funktion som används för att skapa en nyckel som kan sortera metadataobjekt genom deras relevans givet en sökförfråga (titel, författare, kännetecken).

Dessa nycklar används för att sortera resultatet av ett anrop till identify().

För mer information om standardalgoritmen, se: klass InternalMetadataCompareKeyGen. Återimplementera denna funktion i din insticksmodul om standardalgoritmen inte är lämplig.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[source]

Identifiera en bok med dess titel/författare/ISBN/osv.

Om en eller flera identifierare anges och ingen träff hittas och den här metadatakällan inte lagrar alla relaterade identifierare (till exempel alla ISBN av en bok), bör den här metoden försökas igen med bara titel och författare (förutsatt att de specificerades).

Om den här metadatakällan också innehåller omslag, bör URL:en till omslaget cachas så att ett efterföljande anrop till skaffa omslags-API:et med samma ISBN/specialidentifiering inte behöver hämta omslags-URL:en igen. Använd cache-API:et för detta.

Varje metadataobjekt infört i result_queue med denna metod måste ha en source_relevance attribut som är ett heltal som anger i vilken ordning resultaten återgavs av metadatakällan för den här förfrågan. Detta heltal kommer att användas av compare_identify_results(). Om ordningen är oviktigt, sätt den till noll för varje resultat.

Se till att all omslags-/ISBN-mappningsinformation cachelagras innan Metadata-objektet placeras i result_queue.

Parametrar:

- log – En loggpost, använd den för att mata ut felsökningsinformation/fel

- result_queue – Ett resultat Kö, resultaten läggas in i den. Varje resultat är ett metadataobjekt

- abort – Om abort.is_set() returnerar True, avbryt vidare bearbetning och återvända så snart som möjligt

- title – Titeln på boken, kan vara None

- authors – En lista med författare av boken, kan vara None

- identifiers – En ordbok av andra identifieringsuppgifter oftast {’isbn’:’1234…’}

- timeout – Tidsbegränsning i sekunder, ingen nätverksförfrågan ska dröja längre än tidsbegränsningen.

Returnerar:

None om inga fel har inträffat, annars en Unicode-representation av felet som är lämplig för att visa för användaren

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[source]

Hämta ett omslag och sätta det i result_queue. Parametrarna alla har samma betydelse som för identify(). Put (jaget, cover_data) i result_queue.

Den här metoden bör använda cachade omslags-URL:er för effektivitet när det är möjligt. När cachade data inte finns, anropar de flesta insticksmoduler helt enkelt identifiering och använder dess resultat.

Om parametern get_best_cover är True, och kan denna insticksmodul få flera omslag, det bör bara få det ”bästa”.

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[source]

Skapa en slags nyckel för jämförelse av relevansen av metadataobjekt, ges en sökförfråga. Detta används endast för att jämföra resultat från samma metadatakälla, inte mellan olika informationskällor.

Sorteringsnyckeln säkerställer att en stigande ordning sortera är en sortera fallande relevans.

Algoritmen är:

- Föredra resultat som har åtminstone en identifierare som är samma som för förfrågan

- Föredra resultat med en cachad omslags-URL

- Föredra resultat med alla tillgängliga fält ifyllda

- Föredra resultat med samma språk som aktuella användarens gränssnittsspråk

- Föredra resultat som är en exakt titelmatchning på förfrågan

- Föredra resultat med längre kommentarer (mer än 10% längre)

- Använd relevansen av resultatet som rapporterats av metadatakällans sökning

engine

Konvertering insticksmodul
class calibre.customize.conversion.InputFormatPlugin(*args)[source]

Grunder: Plugin

InputFormatPlugins ansvarar för att konvertera ett dokument till HTML+OPF+CSS+osv. Resultatet av konverteringen måste kodas i UTF-8. Den viktigaste åtgärden händer i convert().

type = 'Konverteringsinmatning'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

can_be_disabled = False

Om False, kommer inte användaren kunna deaktivera denna insticksmodul. Använd med försiktighet.

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

file_types = {}

Uppsättning med filtyper som denna insticksmodul ska köra Till exempel: set(['azw', 'mobi', 'prc'])

is_image_collection = False

Om True, denna inmatning insticksmodul skapar en samling bilder, en per HTML-fil. Detta kan ställas in dynamiskt, i konverteringsmetoden om inmatningsfilerna kan vara både bildsamlingar och icke-bildsamlingar. Om du ställer in detta till True måste du implementera get_images () som returnerar en lista med bilder.

core_usage = 1

Antal CPU-kärnor som används av denna insticksmodul. Ett värde av -1 betyder att den använder alla tillgängliga kärnor

for_viewer = False

Om värdet är True, kommer inmatning insticksmodulen specialbearbeta att göra sitt utmatning lämpligt för visning

output_encoding = 'utf-8'

Kodningen som denna inmatning insticksmodul skapar filer i. Ett värde på None betyder att kodningen är odefinierad och måste upptäckas var för sig

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Alternativ som delas av alla inmatningsformatsinsticksmoduler. Åsidostätt inte i underklasser. Använd options istället. Varje alternativ ska vara en instans av OptionRecommendation.

options = {}

Alternativ för att anpassa beteendet för denna insticksmodul. Varje alternativ ska vara en instans av OptionRecommendation.

recommendations = {}

En uppsättning av 3-tupler av formen (option_name, recommended_value, recommendation_level)

get_images()[source]

Returnera en lista med absoluta sökvägar till bilderna, om denna inmatning insticksmodul representerar en bildsamling. Listan med bilder är i samma ordning som bokryggen och innehållsförteckningen.

convert(stream, options, file_ext, log, accelerators)[source]

Denna metod måste implementeras i underklasser. Den måste returnera sökvägen till den skapade OPF-filen eller en OEBBook-instans. All utmatning ska ingå i den aktuella mappen. Om denna insticksmodul skapar filer utanför den aktuella mappen måste de tas bort/markeras för borttagning innan denna metod avslutas.

Parametrar:

- stream – En fil som objekt som innehåller inmatningsfilen.

- options – Alternativ för att anpassa konverteringen. Garanterat att ha attribut som motsvarar alla de alternativ som deklarerats av denna insticksmodul. Dessutom kommer det att ha en utförlig attribut som tar intergralvärden från noll och uppåt. Högre siffror innebär att vara mer utförlig. En annan användbar egenskap är input_profile det är en instans av calibre.customize.profiles.InputProfile.

- file_ext – Filtypsändelsen (utan .) i inmatningsfilen. Det är garanterat att vara en av de file_types stöds av denna insticksmodul.

- log – Ett calibre.utils.logging.Log objekt. All utmatning bör använda detta objekt.

- accelarators – En ordbok för olika uppgifter inmatning insticksmodul kan få lätt som skulle kunna påskynda de efterföljande stegen i konverteringen.

postprocess_book(oeb, opts, log)[source]

Anropas för att tillåta inmatning insticksmodul för att utföra efterbehandling efter att boken har analyserats.

specialize(oeb, opts, log, output_fmt)[source]

Anropas för att tillåta inmatning insticksmodul att specialisera den analyserade boken för en viss utmatning format. Anropas efter postprocess_book och innan några omvandlingar utförs på den analyserade boken.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Anropas för att skapa gränssnittskomponenten som används för att anpassa denna insticksmodul i calibre-användaregränssnittet. Gränssnittskomponenten måste vara en instans av klassen PluginWidget. Se inbyggda inmatningsinsticksmoduler för exempel.

class calibre.customize.conversion.OutputFormatPlugin(*args)[source]

Grunder: Plugin

OutputFormatPlugins är ansvariga för att konvertera ett OEB-dokument (OPF+HTML) till e-bokutmatning.

OEB-dokumentet kan antas vara kodade i UTF-8. Den viktigaste åtgärden händer i convert().

type = 'Konverteringsutmatning'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

can_be_disabled = False

Om False, kommer inte användaren kunna deaktivera denna insticksmodul. Använd med försiktighet.

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

file_type = None

Filtypen (ändelse utan främre punkt) att denna insticksmodul utmatning

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Alternativ som delas av alla inmatningsformatsinsticksmoduler. Åsidostätt inte i underklasser. Använd options istället. Varje alternativ ska vara en instans av OptionRecommendation.

options = {}

Alternativ för att anpassa beteendet för denna insticksmodul. Varje alternativ ska vara en instans av OptionRecommendation.

recommendations = {}

En uppsättning av 3-tupler av formen (option_name, recommended_value, recommendation_level)

property description

str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Skapa ett nytt strängobjekt från det givna objektet. Om kodning eller fel anges måste objektet exponera en databuffert som kommer att avkodas med den givna kodningen och felhanteraren. Annars returneras resultatet av object.__str__() ((om definierat)) eller repr(object). kodning är som standard sys.getdefaultencoding(). fel är som standard ’strict’.

convert(oeb_book, output, input_plugin, opts, log)[source]

Återge innehållet i oeb_book (som är en instans av: calibre.ebooks.oeb.OEBBook) till filen som anges av utgång.

Parametrar:

- output – Antingen en fil som ett objekt eller en sträng. Om det är en sträng är det sökvägen till en mapp som kanske eller kanske inte finns. Utmatningsinsticksmodulen ska skriva dess utmatning till den mappen. Om det är en fil som objekt ska utmatningsinsticksmodulen skriva dess utmatning till filen.

- input_plugin – Inmatningsinsticksmodulen som användes i början av konverteringsprocessen.

- opts – Konverteringsalternativ. Garanterat att ha attribut som motsvarar OptionRecommendations av denna insticksmodul.

- log – Loggern. Skriv ut felsöknings/infomeddelanden osv. med hjälp av denna.

specialize_options(log, opts, input_fmt)[source]

Kan användas för att ändra värdena för konverteringsalternativ, som används av konverteringsprocessen.

specialize_css_for_output(log, opts, item, stylizer)[source]

Kan användas för att göra ändringar i CSS under CSS-utplattningsprocessen.

Parametrar:

- item – Elementet (HTML-fil) som bearbetas

- stylizer – Ett stiliseringsobjekt som innehåller de utplattade stilarna för posten. Du kan få stilen för alla element av stylizer.style(element).

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Anropas för att skapa gränssnittskomponenten för att anpassa denna insticksmodul i calibres användargranssnittet. Gränssnittskomponenten måste vara en instans av klassen PluginWidget. Se inbyggda utmatningsinsticksmoduler för exempel.

Enhetsdrivrutiner

Basklass för alla drivrutiner är DevicePlugin. Men om din enhet exponerar sig som en USBMS-enhet till operativsystemet, bör du använda USBMS-klassen istället eftersom den genomför all logik som behövs för att stöda dessa typer av enheter.

class calibre.devices.interface.DevicePlugin(plugin_path)[source]

Grunder: Plugin

Definierar gränssnitt som ska implementeras av bakomliggande system som kommunicerar med en e-bokläsenhet.

type = 'Enhetsgränssnitt'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

Ordnad lista med format som stöds

VENDOR_ID = 0

VENDOR_ID kan vara antingen ett heltal, en lista med heltal eller ett ordbok om det är ett ordbok, måste det vara en ordbok av ordböcker, på formen:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Ett heltal eller en lista med heltal

BCD = None

BCD kan vara antingen None att inte skilja mellan enheter baserade på BCD, eller det kan vara en lista med BCD-numren för alla enheter som stöds av denna drivrutin.

THUMBNAIL_HEIGHT = 68

Höjd för omslagsminiatyrbilder på enheten

THUMBNAIL_COMPRESSION_QUALITY = 75

Komprimeringskvalitet för miniatyrbilder. Ställ in detta till närmare 100 för att få bättre kvalitet på miniatyrbilder med färre komprimeringsartefakter. Naturligtvis, miniatyrbilden blir större också.

WANTS_UPDATED_THUMBNAILS = False

Ändra den till True om enheten stödjer uppdatering av omslagsminiatyrbilder under sync_booklists. Ställ in den till True kommer att be device.py att uppdatera omslagsminiatyrbilder under bokmatchning

CAN_SET_METADATA = ['title', 'authors', 'collections']

Huruvida metadata om böckerna kan ställas in via GUI.

CAN_DO_DEVICE_DB_PLUGBOARD = False

Hurvida enheten kan hantera device_db-metadatapluggbrädor

path_sep = '/'

Sökvägsavskiljare för sökvägar till böcker på enheten

icon = 'reader.png'

Ikon för denna enhet

UserAnnotation

alias of Annotation

OPEN_FEEDBACK_MESSAGE = None

Det grafiska användargränssnittet visar detta som ett meddelande om inte None i statusfältet. Användbart om öppning kan ta lång tid

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

Uppsättning av tillägg som är ”virtuella böcker” på enheten och kan därför inte ses/sparas/läggas till biblioteket. Till exempel: frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

Meddelande att visa för användare av virtuella boktillägg.

NUKE_COMMENTS = None

I fall det ska förstöra kommentarer i kopian av boken som skickas till enheten. Om inte None bör detta vara kort sträng som kommentarerna kommer att ersättas av.

MANAGES_DEVICE_PRESENCE = False

Om True indikerar att den här drivrutinen helt hanterar enhetsupptäckt, utmatning och så vidare. Om du ställer in detta till True, måste du implementera detect_managed_devices och debug_managed_device_detection metoder. En drivrutin med denna inställning som True är ansvarig för att upptäckt av enheter, hantering av en svartlista över enheter, en lista över utmatade enheter och så vidare. calibre kommer med jämna mellanrum att anropa metoden detect_managed_devices() och om det returnerar en upptäckt enhet kommer calibre att anropa open(). open() kommer att anropas varje gång en enhet returneras även om tidigare anrop till open() misslyckades, därför måste drivrutinen bibehålla sin egen svartlista över misslyckade enheter. Likaså vid utmatning, kommer calibre att anropa eject() och sedan anta att nästa anrop till detect_managed_devices() returnerar None, anropar den post_yank_cleanup().

SLOW_DRIVEINFO = False

Om värder är True, kommer calibre att anropa get_driveinfo() metoden efter att boklistor har lästs för att få ”driveinfo”.

ASK_TO_ALLOW_CONNECT = False

Om värdet är True, kommer calibre fråga användaren om de vill hantera enheten med calibre, första gången den upptäcktes. Om du ställer in detta till True måste du implementera get_device_uid() och ignore_connected_device() och get_user_blacklisted_devices() och set_user_blacklisted_devices()

user_feedback_after_callback = None

Ställ in detta i en ordbok med formuläret {’title’:title, ’msg’:msg, ’det_msg’:detailed_msg} för att calibre ska få ett meddelande till användaren efter att några återanrop har körts (för närvarande endast upload_books). Var försiktig så att du inte skräppostar användaren med för många meddelanden. Denna variabel kontrolleras efter varje anrop, så ställ in den bara när du verkligen behöver.

classmethod get_open_popup_message()[source]

Det grafiska användargränssnittet visar detta som en icke-modal popup. Bör vara en instans av OpenPopupMessage

is_usb_connected(devices_on_system, debug=False, only_presence=False)[source]

Returnera True, device_info om en enhet som hanteras av denna insticksmodul är ansluten.

Parametrar:

devices_on_system – Lista med för närvarande anslutna enheter

detect_managed_devices(devices_on_system, force_refresh=False)[source]

Anropas bara om MANAGES_DEVICE_PRESENCE är True.

Skanna efter enheter som denna drivrutin kan hantera. Ska returnera en enhetsobjekt om en enhet hittas. Detta syfte kommer att vidarebefordras till open() metoden som connected_device. Om ingen enhet hittas, retruneras None. Den återgivna objektet kan vara vad som helst, calibre använder inte den, det skickas bara till open().

Denna metod anropas periodiskt av användargränssnittet, så se till att den inte är för resurskrävande. Använd en cache för att undvika att upprepade gånger skanna systemet.

Parametrar:

- devices_on_system – Uppsättning av USB-enheter hittade på systemet.

- force_refresh – Om True och drivrutinen använder en cache för att förhindra upprepad skanning måste cachen tömmas.

debug_managed_device_detection(devices_on_system, output)[source]

Anropas bara om MANAGES_DEVICE_PRESENCE är True.

Bör skriva information om enheterna som upptäckts på systemet till utmatning, vilket är en fil som objekt.

Ska returnera True om en enhet upptäcktes och öppnades, annars False.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Parametrar:

- key – Nyckeln för att låsa upp enheten

- log_packets – Om True paketströmmen till/från enheten loggas

- report_progress – Funktion som anropas med %-förlopp (tal mellan 0 och 100) för olika uppgifter. Om den anropas med -1 betyder det att uppgiften inte har någon förloppsinformation

- detected_device – Enhetsinformation från enhetsskannaren

can_handle_windows(usbdevice, debug=False)[source]

Valfri metod för att utföra ytterligare kontroller på en enhet för att se om den här drivrutinen kan hantera det. Om den inte är det ska den returnera False. Den här metoden anropas först efter att leverantören, produkt-ID:et och bcd:et har matchats, så det kan göra några relativt tidskrävande kontroller. Standardimplementeringen returnerar True. Den här metoden anropas bara på Windows. Se även can_handle().

Observera att för enheter baserade på USBMS denna metod delegerar som standard till can_handle(). Så du behöver bara åsidosätta can_handle() i din underklass av USBMS.

Parametrar:

usbdevice – En USB-enhet som returneras av calibre.devices.winusb.scan_usb_devices()

can_handle(device_info, debug=False)[source]

Unix-version av can_handle_windows().

Parametrar:

device_info – Är en tupel av (VID, pid, BCD, tillverkare, produkt, serienummer)

open(connected_device, library_uuid)[source]

Utför specifik initiering på alla enheter. Anropas efter att enheten har upptäckts men innan några andra funktioner kommunicerar med enheten. Till exempel: För enheter som presenterar sig som USB-masslagringsenheter, skulle denna metod vara ansvarig för montering av enheten eller om enheten har en automatisk montering, för att ta reda på var det har monterats. Metoden calibre.devices.usbms.device.Device.open() har en implementering av denna funktion som bör tjäna som ett gott exempel för USB-masslagringsenheter.

Denna metod kan indikera ett OpenFeedback-undantag för att visa ett meddelande till användaren.

Parametrar:

- connected_device – Enheten som vi försöker öppna. Det är en typ av (leverantörs-ID, produkt-id, bcd, tillverkarens namn, produktnamn, enhetens serienummer). Vissa enheter har emellertid inget serienummer och på Windows finns bara de tre första fälten, resten är None.

- library_uuid – UUID:et för det aktuella calibre-biblioteket. Kan vara None om det inte finns något bibliotek (till exempel när det används från kommandoraden).

eject()[source]

Avmontera/mata ut enheten från OS. Detta innebär inte kontroll om det finns väntande jobb som behöver kommuniceras med enheten.

OBS: Att denna metod inte kan anropas på samma tråd som resten av enhetsmetoderna.

post_yank_cleanup()[source]

Anropas om användaren rycker enheten utan att mata ut det först.

set_progress_reporter(report_progress)[source]

Ändra en funktion för att rapportera förloppsinformationen.

Parametrar:

report_progress – Funktion som anropas med %-förlopp (tal mellan 0 och 100) för olika uppgifter. Om den anropas med -1 betyder det att uppgiften inte har någon förloppsinformation

get_device_information(end_session=True)[source]

Fråga enheten efter enhetsinformation . Se L{DeviceInfoQuery}.

Returnerar:

(enhetsnamn, enhetsversion, mjukvaruversion på enheten, MIME-typ) Tupeln kan valfritt ha ett femte element, som är en enhetsinformationsordbok. Se usbms.driver för ett exempel.

get_driveinfo()[source]

Returnera driveinfo-ordboken. Anropas vanligtvis från get_device_information(), men om inläsningen av driveinfo är långsam för den här drivrutinen, bör den ställa in SLOW_DRIVEINFO. I det här fallet kommer den här metoden at anropas av calibre efter att boklistorna har lästs in. Observera att den inte anropas på enhetstråden, så drivrutinenbör cachelagra enhetsinformationen i metoden books() och den här funktionen bör returnera cachade data.

card_prefix(end_session=True)[source]

Returnera en lista med 2 element med prefixet till sökvägar på korten. Om inget kort är förinställt ställs None in som kortets prefix. Till exempel (’/plats’, ’/plats2’) (None, ’plats2’) (’plats’, None) (None, None)

total_space(end_session=True)[source]

Få total utrymme på monteringspunkter:

1. Huvudminne

2. Minneskort A

3. Minneskort B

Returnerar:

En 3-elementlista med totalt utrymme i oktetter av (1, 2, 3). Om en viss enhet inte har någon av dessa platser den ska visa 0.

free_space(end_session=True)[source]

Få ledigt utrymme på monteringspunkter:

1. Huvudminne

2. Kort A

3. Kort B

Returnerar:

En 3-elementlista med fritt utrymme i oktetter av (1, 2, 3). Om en viss enhet inte har någon av dessa platser den ska returnera -1.

books(oncard=None, end_session=True)[source]

Returnera en lista med e-böcker på enheten.

Parametrar:

oncard – Om ”carda” eller ”cardb” returnerar en lista med e-böcker på det specifika minneskortet, annars returneras en lista med e-böcker i huvudminnet av enheten. Om ett kort är specificerad och inga böcker finns på kortet returneras en tomma listan.

Returnerar:

En BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Överför en lista med böcker till enheten. Om en fil redan finns på enheten, ska den ersättas. Denna metod bör signalera ett FreeSpaceError om det inte finns tillräckligt med ledigt utrymme på enheten. Texten till FreeSpaceError måste innehålla ordet ”kort” om on_card inte None annars måste innehålla ordet ”minne”.

Parametrar:

- files – En lista med sökvägar

- names – En lista med filnamn som böckerna skulle ha när väl överförda till enheten. len(names) == len(files)

- metadata – Om inte None, det är en lista med Metadata objekt. Tanken är att använda metadata för att avgöra var på enheten att lägga boken. len(metadata) == len(files). Bortsett från det vanliga omslaget (sökväg för omslaget), kan det också finnas ett omslagsminiatyrbildattribut, som ska användas istället. Omslagsminiatyrbildattributet är på formen (bredd, höjd, cover_data som jpeg).

Returnerar:

En lista med 3-elements tupler. Listan är tänkt att skickas till add_books_to_metadata().

classmethod add_books_to_metadata(locations, metadata, booklists)[source]

Lägg till platser i boklistorna. Denna funktion får inte kommunicera med enheten.

Parametrar:

- locations – Resultat av ett anrop till L{upload_books}

- metadata – Lista med Metadata objekt, samma som för upload_books().

- booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

delete_books(paths, end_session=True)[source]

Ta bort böcker i sökvägar på enhet.

classmethod remove_books_from_metadata(paths, booklists)[source]

Ta böcker från metadatalistan. Denna funktion får inte kommunicera med enheten.

Parametrar:

- paths – sökvägar till böcker på enheten.

- booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

sync_booklists(booklists, end_session=True)[source]

Uppdatera metadata på enheten.

Parametrar:

booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

get_file(path, outfile, end_session=True)[source]

Läs filen på sökväg på enheten och skriva den till utfil.

Parametrar:

outfile – filobjekt som sys.stdout eller resultatet av ett open() anrop.

classmethod config_widget()[source]

Bör returnera en QWidget-gränssnittskomponent. QWidget-gränssnittskomponent innehåller inställningarna för enhetens gränssnitt

classmethod save_settings(settings_widget)[source]

Ska spara inställningarna på disken. Tar gränssnittskomponenten skapad i config_widget() och sparar alla inställningar på disken.

classmethod settings()[source]

Ska returnera ett opts objekt. Den opts-objektet bör ha minst ett attribut format_map vilket är en ordnad lista med format för enheten.

set_plugboards(plugboards, pb_func)[source]

tillhandahåll drivrutinen den aktuella uppsättningen av pluggbrädor och en funktion för att välja pluggbräda. Denna metod anropas omedelbart före add_books och sync_booklists.

pb_func är anropbar med följande signatur:

def pb_func(device_name, format, pluggbrädor)

Du ger den det aktuella enhetsnamnet (antingen klassnamnet eller DEVICE_PLUGBOARD_NAME), formatet du är intresserad av (ett ”riktigt” format eller ”device_db”) och de pluggbrädor (du fick dem av set_plugboards, på samma plats som du fick den här metoden).

Returnerar:

Ingen eller en pluggbrädsinstans.

set_driveinfo_name(location_code, name)[source]

Ange enhetens namn i driveinfo-filen till ”namn”. Inställningen kvarstår tills filen återskapas eller namnet ändras igen.

Icke-diskenheter bör genomföra denna metod baserad på platskoder som returneras av get_device_information() metoden.

prepare_addable_books(paths)[source]

Genom att få en lista med sökvägar, returneras en annan lista med sökvägar. Dessa sökvägar hänvisar till tilläggningsbara versioner av böckerna.

Om det finns ett fel vid förberedning av en bok, så i stället för en sökväg, den position i den återgivna listan för att boken ska vara en tre tupel: (original_path, undantaget instans, återsöknings)

startup()[source]

Anropas när calibre starar enheten. Gör godtycklig initiering som krävs. Observera att flera instanser av klassen kan instansieras, och därför kan __init__ anropas flera gånger, men bara en instans kommer att anropa denna metod. Denna metod anropas på enhetens processtråd, inte på tråden för det grafiska gränssnittet.

shutdown()[source]

Anropas när calibre stängs, antingen för gott eller under förberedelser för omstart. Gör den sanering som krävs. Denna metod anropas på enhetens processtråd, inte tråden för det grafiska gränssnittet.

get_device_uid()[source]

Måste returnera ett unikt id för den för närvarande anslutna enheten (detta anropas omedelbart efter ett lyckat anrop till open()). Du måste implementera den här metoden om du ställer in ASK_TO_ALLOW_CONNECT = True

ignore_connected_device(uid)[source]

Borde ignorera enheten som identifieras av uid (resultatet av ett anrop till get_device_uid()) i framtiden. Du måste implementera denna metod om du ställer in ASK_TO_ALLOW_CONNECT = True. Observera att denna funktion anropas direkt efter open(), så om open() buffrar vissa tillstånd, bör drivrutinen återställa det tillståndet.

get_user_blacklisted_devices()[source]

Returnera karta över enhet uid till kortnamn för alla enheter som användaren har begärt att få ignorerade.

set_user_blacklisted_devices(devices)[source]

Ställ in listan med enheters uid:er som ska ignoreras av denna drivrutin.

specialize_global_preferences(device_prefs)[source]

Implementera denna metod om din enhet vill åsidosätta en viss preferens. Du måste se till att alla anrops platser som vill ha en inställning som kan åsidosättas använder device_prefs [’något’] istället för prefs[’något’]. Din metod ska anropa device_prefs.set_overrides(pref = val, pref = val, …). För närvarande används för: metadata management (prefs[’manage_device_metadata’])

set_library_info(library_name, library_uuid, field_metadata)[source]

Implementera den här metoden om du vill ha information om det aktuella calibre-biblioteket. Denna metod anropas vid start och när calibre-biblioteket ändras medan det är anslutet.

is_dynamically_controllable()[source]

Anropas av enhetshanteraren vid start av insticksmodul. Om den här metoden returnerar en sträng, då a) den stödjer enhetshanteraren dynamiska kontroll gränssnitt, och b) det namnet ska användas när man talar med insticksmodulen.

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

start_plugin()[source]

Denna metod anropas för att starta insticksmodulen. Insticksmodulen ska börja acceptera enhetsanslutningar men det gör det. Om insticksmodulen redan acceptera anslutningar, så görs ingenting.

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

stop_plugin()[source]

Denna metod anropas för att stoppa insticksmodulen. Insticksmodulen ska inte längre acceptera anslutningar och bör städa upp efter sig. Det är troligt att denna metod ska anropa avstängning. Om insticksmodulen redan inte accepterar anslutningar, görs ingenting.

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

get_option(opt_string, default=None)[source]

Returnera värdet av optionen indikerad av opt_string. Den här metoden kan anropas när insticksmodulen inte startats. Returnera None om alternativet inte finns.

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

set_option(opt_string, opt_value)[source]

Ange värdet av alternativ som indikeras av opt_string. Den här metoden kan anropas när insticksmodulen inte startats.

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

is_running()[source]

Returnera True om insticksmodulen startas, annars falsk

Den här metoden kan anropas i GUI-processtråden. En drivrutin som implementerar den här metoden måste vara trådsäkert.

synchronize_with_db(db, book_id, book_metadata, first_call)[source]

Anropas vid bokmatchning när en bok på enheten matchar en bok i calibres databas. Metoden ansvarar för att synkronisera data från enheten till calibres databas (om det behövs).

Metoden ska returnera en tvåvärdig tupel. Det första värdet är en uppsättning av calibre-bok-id:et som ändras om calibres databas ändrades eller None om databasen inte ändrades. Om det första värdet är en tom uppsättning uppdateras metadata för boken på enheten med calibres metadata och ges tillbaka till enheten, men ingen grafisktgränsnittsuppdatering av den boken sker. Detta är användbart när calibre-data är korrekt men måste skickas till enheten.

Det andra värdet är i sig en 2-värde tupel. Det första värdet i tupeln anger om ett bokformat ska skickas till enheten. Syftet är att tillåta verifiering av att boken på enheten är densamma som boken i calibre. Detta värde måste vara ”None” om ingen bok ska skickas, annars returnera basfilnamnet på enheten (en sträng som foobar.epub). Se till att inkludera ändelsen i namnet. Enhetsundersystemet kommer att skapa ett send_books-jobb för alla böcker med ”not-None” återgivna värden. Observera: Förutom att senare hämta tillägget ignoreras namnet i fall där enheten använder en mall för att skapa filnamnet, vilket de flesta gör. Det andra värdet i den återgivna tupeln indikerade om formatet är framtidsdaterat. Returnera True om det är, annars returnera False. calibre visar en dialogruta för användaren som listar alla framtidsdaterade böcker.

Extremt viktigt: den här metoden anropas på GUI-processtråden. Den måste vara trådsäker i förhållande till enhetens huvudtråd.

book_id: calibre id för boken i databasen. book_metadata: Metadataobjektet för boken som kommer från enheten. first_call: True om det är första anropet under en synkronisering, False annars

class calibre.devices.interface.BookList(oncard, prefix, settings)[source]

Grunder: list

En lista med böcker. Varje bokpost måste ha fälten

1. titel

2. författare

3. storlek (filstorlek av boken)

4. datumtid (en UTC-tids tupel)

5. sökväg (sökväg på enheten till boken)

6. omslagsminiatyrbild (kan vara None) omslagsminiatyrbild är antingen ett str/byte-objekt med bilddata eller det borde ha ett attribut image_path som lagrar en absolut (plattformsberoende) sökvägen till bilden

7. taggar (en lista med strängar, kan vara tom).

supports_collections()[source]

Returnera True om enheten stöder kollektioner för denna boklista.

add_book(book, replace_metadata)[source]

Lägg boken i boklistan. Avsikten är att behålla alla enheters interna metadata. Returnera True om boklistor måste synkroniseras

remove_book(book)[source]

Ta bort en bok från boklistan. Rätta någon enhets metadata samtidigt

get_collections(collection_attributes)[source]

Returnera en ordbok med samlingar som skapats från collection_attributes. Varje post i ordboken är på formen kollektionsnamn:[lista med böcker]

Listan med böcker sorterad efter boktitel, förutom samlingar som skapats från serien, i vilket fall series_index används.

Parametrar:

collection_attributes – En lista med attribut av bokpost

USB-masslagringsbaserade enheter

Basklass för sådan enhet är calibre.devices.usbms.driver.USBMS. Denna klass ärver i sin tur en del av dess funktioner från sina baser, dokumenterad nedan. En typisk grund USBMS baserad drivrutin ser ut så här:

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]

Grunder: DeviceConfig, DevicePlugin

Denna klass ger logik som är gemensam för alla drivrutiner för enheter som exporterar själva som USB-masslagringsenheter. Ger implementationer för montering/utmatning av USBMS-enheter på alla plattformar.

VENDOR_ID = 0

VENDOR_ID kan vara antingen ett heltal, en lista med heltal eller ett ordbok om det är ett ordbok, måste det vara en ordbok av ordböcker, på formen:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Ett heltal eller en lista med heltal

BCD = None

BCD kan vara antingen None att inte skilja mellan enheter baserade på BCD, eller det kan vara en lista med BCD-numren för alla enheter som stöds av denna drivrutin.

WINDOWS_MAIN_MEM = None

Sträng som identifierar enhetens huvudminne i Windows PnP-id-strängarna Detta kan vara Inga, sträng, lista över strängar eller kompilerad regex

WINDOWS_CARD_A_MEM = None

Sträng som identifierar enhetens första kort i Windows PnP-id-strängarna Detta kan vara None, sträng, lista över strängar eller kompilerad regex

WINDOWS_CARD_B_MEM = None

Sträng som identifierar enhetens andra kort i Windows PnP-id-strängarna Detta kan vara Inga, sträng, lista över strängar eller kompilerad regex

OSX_MAIN_MEM_VOL_PAT = None

Används av den nya drivrutinens upptäckaren för att klargöra huvudminnet från minneskort. Bör vara ett reguljärt uttryck som matchar huvudminnets monteringspunkt tilldelad av macOS

BACKLOADING_ERROR_MESSAGE = None

MAX_PATH_LEN = 250

Största längd av sökvägar som skapas på enheten

NEWS_IN_FOLDER = True

Lägg nyheter i sin egen mapp

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Parametrar:

- key – Nyckeln för att låsa upp enheten

- log_packets – Om True paketströmmen till/från enheten loggas

- report_progress – Funktion som anropas med %-förlopp (tal mellan 0 och 100) för olika uppgifter. Om den anropas med -1 betyder det att uppgiften inte har någon förloppsinformation

- detected_device – Enhetsinformation från enhetsskannaren

set_progress_reporter(report_progress)[source]

Ändra en funktion för att rapportera förloppsinformationen.

Parametrar:

report_progress – Funktion som anropas med %-förlopp (tal mellan 0 och 100) för olika uppgifter. Om den anropas med -1 betyder det att uppgiften inte har någon förloppsinformation

card_prefix(end_session=True)[source]

Returnera en lista med 2 element med prefixet till sökvägar på korten. Om inget kort är förinställt ställs None in som kortets prefix. Till exempel (’/plats’, ’/plats2’) (None, ’plats2’) (’plats’, None) (None, None)

total_space(end_session=True)[source]

Få total utrymme på monteringspunkter:

1. Huvudminne

2. Minneskort A

3. Minneskort B

Returnerar:

En 3-elementlista med totalt utrymme i oktetter av (1, 2, 3). Om en viss enhet inte har någon av dessa platser den ska visa 0.

free_space(end_session=True)[source]

Få ledigt utrymme på monteringspunkter:

1. Huvudminne

2. Kort A

3. Kort B

Returnerar:

En 3-elementlista med fritt utrymme i oktetter av (1, 2, 3). Om en viss enhet inte har någon av dessa platser den ska returnera -1.

windows_sort_drives(drives)[source]

Anropas för att klargöra huvudminnet och minneskortet för enheter som inte skiljer mellan dem på grundval av WINDOWS_CARD_NAME. Till exempel: EB600

can_handle_windows(usbdevice, debug=False)[source]

Valfri metod för att utföra ytterligare kontroller på en enhet för att se om den här drivrutinen kan hantera det. Om den inte är det ska den returnera False. Den här metoden anropas först efter att leverantören, produkt-ID:et och bcd:et har matchats, så det kan göra några relativt tidskrävande kontroller. Standardimplementeringen returnerar True. Den här metoden anropas bara på Windows. Se även can_handle().

Observera att för enheter baserade på USBMS denna metod delegerar som standard till can_handle(). Så du behöver bara åsidosätta can_handle() i din underklass av USBMS.

Parametrar:

usbdevice – En USB-enhet som returneras av calibre.devices.winusb.scan_usb_devices()

open(connected_device, library_uuid)[source]

Utför specifik initiering på alla enheter. Anropas efter att enheten har upptäckts men innan några andra funktioner kommunicerar med enheten. Till exempel: För enheter som presenterar sig som USB-masslagringsenheter, skulle denna metod vara ansvarig för montering av enheten eller om enheten har en automatisk montering, för att ta reda på var det har monterats. Metoden calibre.devices.usbms.device.Device.open() har en implementering av denna funktion som bör tjäna som ett gott exempel för USB-masslagringsenheter.

Denna metod kan indikera ett OpenFeedback-undantag för att visa ett meddelande till användaren.

Parametrar:

- connected_device – Enheten som vi försöker öppna. Det är en typ av (leverantörs-ID, produkt-id, bcd, tillverkarens namn, produktnamn, enhetens serienummer). Vissa enheter har emellertid inget serienummer och på Windows finns bara de tre första fälten, resten är None.

- library_uuid – UUID:et för det aktuella calibre-biblioteket. Kan vara None om det inte finns något bibliotek (till exempel när det används från kommandoraden).

eject()[source]

Avmontera/mata ut enheten från OS. Detta innebär inte kontroll om det finns väntande jobb som behöver kommuniceras med enheten.

OBS: Att denna metod inte kan anropas på samma tråd som resten av enhetsmetoderna.

post_yank_cleanup()[source]

Anropas om användaren rycker enheten utan att mata ut det först.

sanitize_callback(path)[source]

Tillbakaanrop för att låta enskilda drivrutiner åsidosätta sökvägens sanering som används av create_upload_path().

filename_callback(default, mi)[source]

Tillbakaanrop för att låta enskilda drivrutiner ändra standard filnamnet fastställt av create_upload_path().

sanitize_path_components(components)[source]

Utför någon enhetsspecifik sanering på sökvägskomponenter för filer som överförs till enheten

get_annotations(path_map)[source]

Lösa upp path_map till annotation_map filer som finns på enheten

add_annotation_to_library(db, db_id, annotation)[source]

Lägg till en notering i calibre-biblioteket

class calibre.devices.usbms.cli.CLI[source]

class calibre.devices.usbms.driver.USBMS(plugin_path)[source]

Grunder: CLI, Device

Basklass för alla USBMS-enheter. Implementerar logiken för att skicka/få/uppdatera metadata/buffra metadata/osv.

description = 'Kommunicera med en läsenhet.'

En kort sträng som beskriver vad denna insticksmodul gör

author = 'John Schember'

Författare av denna insticksmodul

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

booklist_class

alias of BookList

book_class

alias of Book

FORMATS = []

Ordnad lista med format som stöds

CAN_SET_METADATA = []

Huruvida metadata om böckerna kan ställas in via GUI.

get_device_information(end_session=True)[source]

Fråga enheten efter enhetsinformation . Se L{DeviceInfoQuery}.

Returnerar:

(enhetsnamn, enhetsversion, mjukvaruversion på enheten, MIME-typ) Tupeln kan valfritt ha ett femte element, som är en enhetsinformationsordbok. Se usbms.driver för ett exempel.

set_driveinfo_name(location_code, name)[source]

Ange enhetens namn i driveinfo-filen till ”namn”. Inställningen kvarstår tills filen återskapas eller namnet ändras igen.

Icke-diskenheter bör genomföra denna metod baserad på platskoder som returneras av get_device_information() metoden.

books(oncard=None, end_session=True)[source]

Returnera en lista med e-böcker på enheten.

Parametrar:

oncard – Om ”carda” eller ”cardb” returnerar en lista med e-böcker på det specifika minneskortet, annars returneras en lista med e-böcker i huvudminnet av enheten. Om ett kort är specificerad och inga böcker finns på kortet returneras en tomma listan.

Returnerar:

En BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Överför en lista med böcker till enheten. Om en fil redan finns på enheten, ska den ersättas. Denna metod bör signalera ett FreeSpaceError om det inte finns tillräckligt med ledigt utrymme på enheten. Texten till FreeSpaceError måste innehålla ordet ”kort” om on_card inte None annars måste innehålla ordet ”minne”.

Parametrar:

- files – En lista med sökvägar

- names – En lista med filnamn som böckerna skulle ha när väl överförda till enheten. len(names) == len(files)

- metadata – Om inte None, det är en lista med Metadata objekt. Tanken är att använda metadata för att avgöra var på enheten att lägga boken. len(metadata) == len(files). Bortsett från det vanliga omslaget (sökväg för omslaget), kan det också finnas ett omslagsminiatyrbildattribut, som ska användas istället. Omslagsminiatyrbildattributet är på formen (bredd, höjd, cover_data som jpeg).

Returnerar:

En lista med 3-elements tupler. Listan är tänkt att skickas till add_books_to_metadata().

upload_cover(path, filename, metadata, filepath)[source]

Överför bokomslag till enheten. Standard implementationen gör ingenting.

Parametrar:

- path – Den fullständiga sökvägen till mappen där den tillhörande boken är placerad.

- filename – Namnet på bokfilen utan ändelse.

- metadata – metadata som hör till boken. Använd metadata.thumbnail för omslag

- filepath – Fullständig sökväg till e-bokfilen

add_books_to_metadata(locations, metadata, booklists)[source]

Lägg till platser i boklistorna. Denna funktion får inte kommunicera med enheten.

Parametrar:

- locations – Resultat av ett anrop till L{upload_books}

- metadata – Lista med Metadata objekt, samma som för upload_books().

- booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

delete_books(paths, end_session=True)[source]

Ta bort böcker i sökvägar på enhet.

remove_books_from_metadata(paths, booklists)[source]

Ta böcker från metadatalistan. Denna funktion får inte kommunicera med enheten.

Parametrar:

- paths – sökvägar till böcker på enheten.

- booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

sync_booklists(booklists, end_session=True)[source]

Uppdatera metadata på enheten.

Parametrar:

booklists – En tupel innehåller resultatet av anrop till (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=’cardb’)`).

classmethod normalize_path(path)[source]

Returnera sökväg med plattformsursprungliga sökvägsavskiljare

Användargränssnittsåtgärder

Om du lägger till din egen insticksmodul i en ZIP-fil, bör du underklassa både InterfaceActionBase och InterfaceAction. load_actual_plugin()-metoden för din InterfaceActionBase-underklass måste returnera ett exemplifierat objekt för din InterfaceBase underklass.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[source]

Baser: QObject

En insticksmodul som representerar en ”åtgärd” som kan tas i grafiska användargränssnittet. Alla post i verktygsfältet och snabbmenyer implementerar av dessa insticksmodul.

Observera att den här klassen är basklassen för dessa insticksmodul, dock att integrera insticksmodulen med calibres insticksmodul system, måste du göra en omslagsklass som refererar till den faktiska insticksmoduln. Se calibre.customize.builtins modul för exempel.

Om två InterfaceAction objekt har samma namn, tar den med högre prioritet företräde.

Underklasser bör implementera genesis(), library_changed(), location_selected(), shutting_down(), initialization_complete() och tag_browser_context_action() metoder.

När initierats, har denna insticksmodul tillgång till calibres huvudanvändargränssnitt via gui medlem. Du kan komma åt andra insticksmodul med namn, till exempel:

self.gui.iactions['Save To Disk']

För att få åktomst till själva insticksmodulen, använd attributet interface_action_base_plugin, detta attribut blir först tillgänglig efter att insticksmodulen har initierats. Användbart om du vill använda metoder från insticksmodulsklassen som do_user_config().

QAction anges av action_spec skapas automatiskt och blir tillgänglig via self.qaction.

name = 'Implement me'

Insticksmodulnamnet. Om två insticksmodul med samma namn är närvarande, tar en med högre prioritet företräde.

priority = 1

Insticksmodulsprioritet. Om två insticksmodul med samma namn är närvarande, tar den med högre prioritet företräde.

popup_type = 1

Menypopuptypen för när denna insticksmodul läggs till i ett verktygsfält

auto_repeat = False

Huruvida denna åtgärd ska upprepas automatiskt när dess genvägsknapp hålls nedtryckt.

action_spec = ('text', 'icon', None, None)

Av formen: (text, icon_path, tooltip, keyboard shortcut). ikon, verktygstips och kortkommandon kan vara None. kortkommandon måste vara antingen en sträng, None eller en rad genvägar. Om None, en kortkommando som motsvarar åtgärden registreras inte. Om du passerar en tom tuppel, registreras genvägen utan någon förinställd tangentbindning.

action_shortcut_name = None

Om inte None, används för namnet som visas för användaren när man anpassar kortkommandon för ovanstående åtgärdsspecifikation istället för action_spec[0]

action_add_menu = False

Om True, skapas en meny automatiskt och läggs till i self.qaction

action_menu_clone_qaction = False

Om True, läggs en klon av self.qaction till menyn för self.qaction Om du vill att texten i den här åtgärden ska vara annorlunda än self.qaction, ställ in den här variabeln till den nya texten

dont_add_to = frozenset({})

Uppsättning av platser som denna åtgärd får inte tilläggas. Se all_locations för en lista med möjliga platser

dont_remove_from = frozenset({})

Uppsättning av platser som denna åtgärd inte får ta bort. Se all_locations för en lista med möjliga platser

action_type = 'global'

Typ av åtgärd ”aktuell” innebär, agerar på den aktuella vyn ”global”: en åtgärd som inte verkar på den aktuella vyn, utan snarare på calibre som helhet

accepts_drops = False

Om True, kommer detta InterfaceAction att ha möjlighet att interagera med dra och släpp händelser. Se de metoder, accept_enter_event(), :meth`:accept_drag_move_event`, drop_event() för mer information.

accept_enter_event(event, mime_data)[source]

Metoden ska returnera True om och endast detta gränssnitt åtgärd kan hantera dra händelsen. Anropa inte acceptera/ignorera vid händelsen, som kommer att tas om hand av den calibre UI.

accept_drag_move_event(event, mime_data)[source]

Metoden ska returnera True om och endast detta gränssnitt åtgärd kan hantera dra händelsen. Anropa inte acceptera/ignorera vid händelsen, som kommer att tas om hand av den calibre UI.

drop_event(event, mime_data)[source]

Denna metod bör utföra några användbara åtgärder och returnera True om endast om denna gränssnittsåtgärd kan hantera händelsen ”drop”. Anropa inte acceptera/ignorera vid händelsen, som kommer att tas om hand av calibre-användargränssnittet. Du bör inte utföra blockerade/långa operationer i denna funktion. Sänd istället en signal eller använd QTimer.singleShot och återvänd snabbt. Se de inbyggda åtgärderna för exempel.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[source]

Bekväm metod för att enkelt lägga handlingar till en QMenu. returnerar den skapade QAction. Denna åtgärd har ett extra attribut calibre_shortcut_unique_name som om den inte None avser det unika namn under vilket denna åtgärd är registrerad med tangentbordsansvarig.

Parametrar:

- menu – Den QMenu som den nyligen skapade åtgärden läggs till i

- unique_name – Ett unikt namn för den här åtgärden, det måste vara globalt unikt, så gör det så beskrivande som möjligt. Om du är osäker, lägg till ett uuid i den.

- text – Texten för denna åtgärd.

- icon – Antingen en QIcon eller ett filnamn. Filnamnet skickas till QIcon.ic() inbyggda, så du behöver inte ange den fullständiga sökvägen till bildmappen.

- shortcut – En sträng, en lista med strängar, None eller False. Om False har ingen tangentbordsgenväg registrerats för denna åtgärd. Om None har ett tangentbordsgenväg med ingen standardtangentbindning registrerats. Sträng och lista med strängar registrerar en genväg med standardtangentbindning som angetts.

- description – En beskrivning för denna åtgärd. Används för att ställa in verktygstips.

- triggered – En anropbar som är ansluten till den utlösta signalen i den skapade åtgärden.

- shortcut_name – Texten som visas för användaren när man anpassar tangentbordsgenvägar för denna åtgärd. Som standard är den inställd på värdet text.

- persist_shortcut – Genvägar för åtgärder som inte alltid visas, eller som är biblioteksberoende, kan försvinna när andra tangentbordsgenvägar redigeras om inte `persist_shortcut` är inställt på True.

load_resources(names)[source]

Om denna insticksmodul kommer i en ZIP-fil (användartillagd insticksmodul), kommer denna metod tillåta dig läsa in resurser från ZIP-fil.

Till exempel att läsa in en bild:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

Parametrar:

names – Förteckning över sökvägar till resurser i ZIP-filen som använder / som avskiljare

Returnerar:

En ordbok med formen {name : file_contents}. Namn som inte finns i ZIP-filen kommer inte att vara närvarande i ordboken.

genesis()[source]

Ställ in denna insticksmodul. Endast anropas en gång under initieringen. self.gui är tillgänglig. Den funktion som specificerats action_spec finns som self.qaction.

location_selected(loc)[source]

Anropas när boklistan som visas i calibre ändras. För närvarande är värden för loc: library, main, card och cardb.

Denna metod bör aktivera/inaktivera denna åtgärd och dess underåtgärder som är lämpliga för platsen.

library_about_to_change(olddb, db)[source]

Anropas var gång det aktuella biblioteket är ändrat.

Parametrar:

- olddb – LibraryDatabasen som motsvarar det förra biblioteket.

- db – LibraryDatabasen som motsvarar det nya biblioteket.

library_changed(db)[source]

Anropas var gång det aktuella biblioteket är ändrat.

Parametrar:

db – LibraryDatabase motsvarar det aktuella biblioteket.

gui_layout_complete()[source]

Anropas en gång per åtgärd när layouten för huvudgränssnittet är klar. Om din åtgärd behöver göra ändringar i layouten bör de göras här snarare än i initialization_complete().

initialization_complete()[source]

Anropas en gång per åtgärd när installationen av huvud-GUI är färdigt.

tag_browser_context_action(index)[source]

Anropas när snabbmenyn visas i taggbläddraren. index är QModelIndex som hänvisar till taggbläddrarposterna som högerklickades. Testa giltigheten med index.valid() och få den underliggande TagTreeItem-posten med index.data(Qt.ItemDataRole.UserRole). Eventuella åtgärdsobjekt som ges av denna metod läggs till i snabbmenyn.

shutting_down()[source]

Anropas en gång per insticksmodul när huvud-GUI är i färd med att stängas av. Släpp alla använda resurser, men försök att inte blockera avstängning under långa tidsperioder.

class calibre.customize.InterfaceActionBase(*args, **kwargs)[source]

Grunder: Plugin

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Författare av denna insticksmodul

type = 'Användargränssnittsåtgärd'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

can_be_disabled = False

Om False, kommer inte användaren kunna deaktivera denna insticksmodul. Använd med försiktighet.

load_actual_plugin(gui)[source]

Denna metod måste returnera själva gränssnittsåtgärden av insticksmodul objektet.

Inställningar insticksmodul
class calibre.customize.PreferencesPlugin(plugin_path)[source]

Grunder: Plugin

En insticksmodul som representerar en gränssnittskomponent som visas i dialogrutan Inställningar.

Denna insticksmodul har bara en viktig metod create_widget(). De olika fälten i insticksmodulen bestämmer hur den kategoriseras i användargränssnittet.

supported_platforms = ['windows', 'osx', 'linux']

Lista över plattformar som den här insticksmodulen fungerar på. Till exempel: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Författare av denna insticksmodul

type = 'Inställningar'

Typen av denna insticksmodul. Används för kategorisera insticksmodul i användargränssnittet

can_be_disabled = False

Om False, kommer inte användaren kunna deaktivera denna insticksmodul. Använd med försiktighet.

config_widget = None

Importera sökväg till modul som innehåller en klass som heter ConfigWidget vilken implementerar ConfigWidgetInterface. Används av create_widget().

category_order = 100

Var i listan av kategorier category av denna insticksmodul bör vara.

name_order = 100

Var i listan av namn i en kategori gui_name som denna insticksmodul bör vara

category = None

Kategorin för denna insticksmodul bör vara i

gui_category = None

Kategorinamnet som visas till användaren för denna insticksmodul

gui_name = None

Namnet som visar till användaren för denna insticksmodul

icon = None

Ikonen för denna insticksmodul bör vara en absolut sökväg

description = None

Beskrivning som används för verktygstips och liknande

create_widget(parent=None)[source]

Skapa och returnerar den faktiska Qt-gränssnittskomponenten som används för ställa in denna grupps inställningar. Gränssnittskomponenten måste implementera calibre.gui2.preferences.ConfigWidgetInterface.

Standard implementationen använder config_widget för att instansiera gränssnittskomponenten.

class calibre.gui2.preferences.ConfigWidgetInterface[source]

Denna klass definierar gränssnittet som alla gränssnittskomponenter som visas i dialogrutan Inställningar måste implementeras. Se ConfigWidgetBase för en basklass som implementerar detta gränssnitt och definierar olika bekvämlighetsmetoder också.

changed_signal = None

Denna signal ska sändas när användaren ändrar ett värde i den här gränssnittskomponenten

supports_restoring_to_defaults = True

Ändra till True om och endast om metoden restore_to_defaults() är implementerad.

restore_defaults_desc = 'Återställ inställningar till standardvärden. Du måste klicka på Tillämpa för att verkligen spara standardinställningarna.'

Verktygstipset för knappen ”Återställ till standardvärden”

restart_critical = False

Om True tillåter inte Inställningsdialogrutan inte användaren att ställa in några fler inställningar. Endast har verkan om commit() returnerar True.

genesis(gui)[source]

Anropas en gång innan gränssnittskomponenten visas, bör utföra alla nödvändiga inställningar.

Parametrar:

gui – calibres huvudanvändargränssnitt

initialize()[source]

Bör ställa in alla konfigurationsvärden till sina ursprungliga värden (värdena lagras i konfigurationsfilerna). Ett ”återgå”-sats är valfritt. Returnera False om dialogrutan inte ska visas.

restore_defaults()[source]

Bör ställa in alla konfigurationsvärden till sina standardvärden.

commit()[source]

Spara eventuella ändrade inställningar. Returnera True om ändringarna kräver omstart, annars False. Höj ett AbortCommit-undantag för att indikera att ett fel har uppstod. Du är ansvarig för att ge användaren återkoppling om vad felet är och hur man åtgärdar det.

refresh_gui(gui)[source]

Anropas en gång efter att den här gränssnittskomponenten har lagts in. Ansvarig för att få det grafiska gränssnittet att läsa om eventuella ändrade inställningar. Observera att som standard återinitierar det grafiska gränssnittets olika element ändå, så de flesta gränssnittskomponenter behöver inte använda den här metoden.

initial_tab_changed()[source]

Anropas om den initialt visade fliken ändras innan gränssnittskomponenten visas, men efter att den har initierats.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[source]

Basklass som innehåller kod för att enkelt lägga till standard konfigureringsgränssnittskomponenter som kryssrutor, kombinationsrutor, textfält och så vidare. se metoden register().

Den här klassen hanterar automatiskt ändringsmeddelande, återställning till standard, översättning mellan objekt i det grafiska gränssnittet och konfigurationsobjekt osv. för registrerade inställningar.

Om din konfigureringsgränssnittskomponent ärver från denna klass men inkluderar inställning som inte är registrerad, bör du åsidosätta ConfigWidgetInterface-metoder och anropa basklassmetoder inuti åsidosättningarna.

changed_signal

Denna signal ska sändas när användaren ändrar ett värde i den här gränssnittskomponenten

supports_restoring_to_defaults = True

Ändra till True om och endast om metoden restore_to_defaults() är implementerad.

restart_critical = False

Om True tillåter inte Inställningsdialogrutan inte användaren att ställa in några fler inställningar. Endast har verkan om commit() returnerar 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]

Registrera en inställning.

Parametrar:

- name – Namnet på inställningen

- config_obj – Konfigureringsobjektet som läser/skriver instälningen

- gui_name – Namnet på det grafiska objekt som presenterar ett gränssnitt för att ändra inställningen. Som standard antas vara 'opt_' + name.

- choices – Om inställningen är en flervals (ComboBox) baserad inställning, listan med alternativ. Listan är en lista på två-elements tupler av formen: [(gui name, value), ...]

- setting – Klassen ansvarar för att hantera den här inställningen. Standard klassen hanterar nästan alla fall, så den här param används sällan.

initialize()[source]

Bör ställa in alla konfigurationsvärden till sina ursprungliga värden (värdena lagras i konfigurationsfilerna). Ett ”återgå”-sats är valfritt. Returnera False om dialogrutan inte ska visas.

commit(*args)[source]

Spara eventuella ändrade inställningar. Returnera True om ändringarna kräver omstart, annars False. Höj ett AbortCommit-undantag för att indikera att ett fel har uppstod. Du är ansvarig för att ge användaren återkoppling om vad felet är och hur man åtgärdar det.

restore_defaults(*args)[source]

Bör ställa in alla konfigurationsvärden till sina standardvärden.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt

Kommandoradsgränssnitt

Observera

På macOS finns kommandoradsverktygen i calibre-paketet, till exempel om du installerade calibre i /Applications finns kommandoradsverktygen i /Applications/calibre.app/Contents/MacOS/. Så, till exempel, för att köra ebook-convert skulle du använda: /Applications/calibre.app/Contents/MacOS/ebook-convert.

Dokumenterade kommandon

- 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

Odokumenterade kommandon

- ebook-device

- markdown-calibre

Du kan se användning av odokumenterade kommandon genom att köra dem utan argument i ett terminalfönster.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibre

calibre

calibre [options] [path_to_ebook or calibre url ...]

Starta huvudanvändargränssnittet i calibre och lägg till e-boken på path_to_ebook till databasen. Du kan också ange URL:er i calibre för att utföra olika åtgärder än att bara lägga till böcker. Till exempel:

calibre://view-book/test_bibliotek/1842/epub

Öppnar boken med id 1842 i EPUB-formatet från biblioteket
”test_bibliotek” i :command:`calibre`s e-bokvisare. Biblioteksnamn är bibliotekens mappnamn med blanksteg ersatta med understrykningar. En fullständig beskrivning av de olika webbadressbaserade åtgärderna finns i användarmanualen.

När du skickar argument till calibre som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--detach

Koppla från kontrollterminalen, om någon (endast Linux)

--help, -h

visa denna hjälptext och avsluta

--ignore-plugins

Ignorera anpassad insticksmodul, användbart om du har installerat en insticksmodul som hindrar calibre från att starta

--no-update-check

Sök inte efter uppdateringar

--shutdown-running-calibre, -s

Orsakar att en körande calibre-instans avslutas, om någon körs. Observera att om det finns pågående jobb kommer de att avbrytas tyst, så använd med försiktighet.

--start-in-tray

Starta minimerad till systemfältet.

--verbose, -v

Ignoreras, ska inte användas. Närvarande endast för arvsskäl

--version

visar programmets versionsnummer och avsluta

--with-library

Använd biblioteket som ligger på den angivna sökvägen.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibre-customize

calibre-customize

calibre-customize options

Anpassa calibre genom att läsa in externa insticksmoduler.

När du skickar argument till calibre-customize som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--add-plugin, -a

Lägg till en insticksmodul genom att ange sökvägen till ZIP-filen som innehåller den.

--build-plugin, -b

För utvecklare av insticksmoduler: Sökvägen till mappen där du utvecklar insticksmodulen. Det här kommandot kommer automatiskt att packa upp insticksmodulen och uppdatera den i calibre.

--customize-plugin

Anpassa insticksmodul. Ange namn på insticksmodul och anpassningssträng separerade med kommatecken. Anpassningssträngen är densamma som du skulle ange när du anpassade insticksmodulen i calibres grafiska huvudanvändargränssnitt.

--disable-plugin

Inaktivera namngiven insticksmodul

--enable-plugin

Aktivera namngiven insticksmodul

--help, -h

visa denna hjälptext och avsluta

--list-plugins, -l

Lista alla installerade insticksmoduler

--remove-plugin, -r

Ta bort en anpassad insticksmodul utifrån dess namn. Har ingen effekt på inbyggda insticksmoduler

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibre-debug

calibre-debug

calibre-debug [options]

Olika kommandoradsgränssnitt som är användbara för felsökning av calibre. Med inga flaggor startar detta kommando en inbäddad Python-tolk. Du kan också köra calibre-huvudgränssnittet, calibres e-bokvisare och calibre redigeraren i felsökningsläge.

Den innehåller också gränssnitt till olika bitar av calibre som inte har dedikerade kommandoradsverktyg, som underuppsättning av teckensnitt, e-bokdiffverktyg och så vidare.

Du kan också använda calibre-debug för att köra fristående skript. För att göra det använder du det så här:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

Allting efter -- skickas till skriptet. Du kan också använda calibre-debug som en shebang i skript, så här:

#!/usr/bin/env -S calibre-debug -e -- –

När du skickar argument till calibre-debug som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--add-simple-plugin

Lägg till en enkel insticksmodul (det vill säga en insticksmodul som består av bara en .py-fil), genom att ange sökvägen till py-filen som innehåller insticksmodulskoden.

--command, -c

Kör Python-kod.

--debug-device-driver, -d

Felsöker enhetsidentifiering

--default-programs

(Av)registrera calibre från Windows-standardprogram. --default-programs = (register|unregister)

--diff

Kör diffverktyg för calibre. Till exempel: calibre-debug --diff fil1 fil2

--edit-book

Starta calibre-verktyget "Redigera book" i felsökningsläge.

--exec-file, -e

Kör Python-koden i fil.

--explode-book, -x

Bryt isär boken i den angivna mappen. Användning: -x file.epub output_dir Exporterar boken som en samling HTML-filer och metadata som du kan redigera med standard HTML-redigeringsverktyg. Fungerar med EPUB-, AZW3-, HTMLZ- och DOCX-filer.

--export-all-calibre-data

Exportera all calibre-data (böcker/inställningar/insticksmoduler). Normalt kommer du att bli tillfrågad om exportmappen och biblioteken att exportera. Du kan också ange dem som kommandoradsargument för att hoppa över frågorna. Använd absoluta sökvägar för exportmappen och biblioteken. Det speciella nyckelordet "all" kan användas för att exportera alla bibliotek. Exempel: calibre-debug --export-all-calibre-data # för interaktiv användning calibre-debug --export-all-calibre-data /sökväg/till/tom/export/mapp /sökväg/till/bibliotek/mapp1 /sökväg/till/bibliotek2 calibre-debug --export-all-calibre-data /export/mapp all # exportera alla kända bibliotek

--fix-multiprocessing

För internt bruk

--gui, -g

Kör det grafiska gränssnittet med felsökning aktiverat. Felsökningsloggar skrivs till stdout och stderr.

--gui-debug

Kör det grafiska gränssnittet med felsökningskonsol, loggning till angiven sökväg. Bara för intern användning, använd -g alternativet för att köra det grafiska gränssnittet i felsökningsläge

--help, -h

visa denna hjälptext och avsluta

--implode-book, -i

Implodera en tidigare exploderad bok. Användning: -i output_dir file.epub Importerar boken från filerna i output_dir som måste ha skapats av ett tidigare anrop till --explode-book. Var noga med att ange samma filtyp som användes vid exploderande.

--import-calibre-data

Importera tidigare exporterade calibre-data

--inspect-mobi, -m

Undersök MOBI-filen eller MOBI-filerna i den angivna sökvägen eller de angivna sökvägarna

--paths

Mata ut nödvändiga sökvägar för att anpassa calibre-miljön

--run-plugin, -r

Kör en insticksmodul som tillhandahåller ett kommandoradsgränssnitt. Till exempel: calibre-debug -r "insticksmodulsnamn" -- fil1 --flagga1 Allt efter -- kommer att skickas till insticksmodulen som argument.

--run-test, -t

Kör de namngivna testerna. Använd specialvärdet "alla" för att köra alla tester. Om testnamnet börjar med en punkt antas det vara ett modulnamn. Om testnamnet börjar med @ antas det vara ett kategorinamn.

--run-without-debug

Kör inte med DEBUG-flaggan inställd

--shutdown-running-calibre, -s

Orsakar att en körande calibre-instans avslutas, om någon körs. Observera att om det finns pågående jobb kommer de att avbrytas tyst, så använd med försiktighet.

--subset-font, -f

Underuppsätt det angivna teckensnittet. Använd -- efter denna flagga för att skicka flaggan till programmet för Underuppsättning av teckensnitt.

--test-build

Testa binärmoduler i bygge

--version

visar programmets versionsnummer och avsluta

--viewer, -w

Kör läsenheten i felsökningsläge

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibre-server

calibre-server

calibre-server [options] [path to library folder...]

Starta calibre-innehållsservern. calibre-innehållsservern exponerar dina calibre-bibliotek över internet. Du kan ange sökvägen till biblioteksmappar som argument till calibre-server. Om du inte anger några vägar, kommer alla biblioteken som i calibre- huvudprogrammet känner till att användas.

När du skickar argument till calibre-server som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--access-log

Sökväg till tillgångs loggfil. Denna loggfil innehåller information om klienter som ansluter till servern och gör efterfrågningar. Som standard görs ingen åtkomstloggning.

--ajax-timeout

Tid (i sekunder) att vänta på ett svar från servern när förfrågningar görs.

--auth-mode

Välj typ av autentisering att använda. Ställ in HTTP-autentiseringsläget som används av servern. Ställ in till "basic" om du placerar den här servern bakom en SSL-proxy. Annars lämnar du det som "auto", vilket kommer att använda "basic" om SSL är anpassat, annars använder det "digest".

--auto-reload

Ladda om servern automatiskt när källkoden ändras. Användbar för utveckling. Du bör också ange ett litet värde för avstängningstiden.

--ban-after

Antal misslyckade inloggningar för uteslutningar. Antal misslyckade inloggningsförsök varefter en IP-adress är utesluten

--ban-for

Uteslut IP-adresser som har upprepade misslyckade inloggningsförsök. Uteslut tillfälligt åtkomst för IP-adresser som har upprepade misslyckade inloggningsförsök under det angivna antalet minuter. Användbart för att förhindra försök att gissa lösenord. Om den är inställd på noll görs ingen uteslutning.

--book-list-mode

Välj standardläge för boklistan. Ställ in standardläget för boklistan som ska användas för nya användare. Enskilda användare kan åsidosätta standardinställningarna i sina egna inställningar. Standardinställningen är att använda ett omslagsrutnät.

--compress-min-size

Minsta storlek för svar att använda vid datakomprimering (i byte).

--custom-list-template

Sökvägen till en JSON-fil som innehåller en mall för det anpassade boklistsläget. Det enklaste sättet att skapa en sådan mallfil är att gå till Inställningar->Dela via nätet->Mall för boklista i calibre, skapa mallen och exportera den.

--daemonize

Kör process i bakgrunden som en demon (endast Linux).

--displayed-fields

Begränsa användardefinierade fält som ska visas. Kommaseparerad lista med användardefinierade metadatafält som kommer att visas av innehållsservern i /opds- och /mobila-vyer. Om du anger det här alternativet kommer alla fält som inte finns i den här listan att inte visas. Till exempel: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

Förallokering av socket-anslutning, till exempel med systemd socket aktivering. Som standard är det här alternativet aktiverat.

--enable-auth, --disable-auth

Lösenordsbaserad autentisering för att komma åt servern. Normalt är servern obegränsad, så att alla kan få åtkomst till den. Du kan begränsa åtkomsten till fördefinierade användare med det här alternativet. Som standard är det här alternativet inaktiverat.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

Återfall till automatiskt upptäckt gränssnitt. Om servern av någon anledning inte kan binda till gränssnittet som anges i alternativet listen_on, kommer det att försöka identifiera ett gränssnitt som ansluter till omvärlden och binda till det. Som standard är det här alternativet aktiverat.

--enable-local-write, --disable-local-write

Tillåt icke-autentiserade lokala anslutningar att göra ändringar. Normalt, om du inte aktiverar autentisering, fungerar servern i skrivskyddat läge för att inte tillåta anonyma användare att göra ändringar i dina calibre-bibliotek. Med det här alternativet kan alla som ansluter från samma dator som servern körs göra ändringar. Detta är användbart om du vill köra servern utan autentisering men ändå använder calibredb för att göra ändringar i dina calibre-bibliotek. Observera att om du aktiverar det här alternativet betyder det att alla program som körs på datorn kan göra ändringar i dina calibre-bibliotek. Som standard är det här alternativet inaktiverat.

--enable-log-not-found, --disable-log-not-found

Logga HTTP 404 (inte hittad)-förfrågningar. Normalt loggar servern alla HTTP-förfrågningar för resurser som inte finns. Detta kan skapa en hel del skräploggar, om din server är inriktad från robotar. Använd det här alternativet för att stänga av dem. Som standard är det här alternativet aktiverat.

--enable-use-bonjour, --disable-use-bonjour

Annonsera OPDS-flöden via BonJour. Annonsera OPDS-flöden via BonJour-tjänsten så att OPDS-baserade läsappar kan upptäcka och ansluta till servern automatiskt. Som standard är det här alternativet aktiverat.

--enable-use-sendfile, --disable-use-sendfile

Nollkopiera filöverföringar för ökad prestanda. Det här kommer att använda nollkopiera för överföringar i kärnan när filer skickas över nätverket, vilket ökar prestandan. Det kan dock orsaka skadade filöverföringar på några brutna filsystem. Om du påträffar skadade filöverföringar, stäng av det. Som standard är det här alternativet aktiverat.

--help, -h

visa denna hjälptext och avsluta

--ignored-fields

Ignorera användardefinierade metadatafält. Kommaseparerad lista över användardefinierade metadatafält som inte kommer att visas av innehållsservern i /opds- och /mobila-vyer. Till exempel: my_rating,my_tags

--listen-on

Gränssnitt för att lyssna efter anslutningar. Standard är att lyssna på alla tillgängliga IPv6- och IPv4-gränssnitt. Du kan ändra detta till till exempel "127.0.0.1" för att bara lyssna efter IPv4-anslutningar från den lokala maskinen, eller till "0.0.0.0" för att lyssna på alla inkommande IPv4-anslutningar.

--log

Sökväg till loggfil för servern. Denna loggfil innehåller serverinformation och fel, inte åtkomstloggar. Som standard skrivs den till stdout.

--manage-users

Hantera databasen över användare som får ansluta till denna server. Du kan använda den i automatiserat läge genom att lägga till en –. Se calibre-server --manage-users -- help för mer information. Se även alternativet --userdb.

--max-header-line-size

Största storlek på enskild HTTP-rubrik (i KB).

--max-job-time

Längsta tid för arbetarprocesser. Högsta tid som arbetsprocesser tillåts köras (i minuter). Ställ in på noll för ingen gräns.

--max-jobs

Högst antal arbetarprocesser. Arbetarprocesser startas efter behov och används för stora jobb som att förbereda en bok för visning, lägga till böcker, konvertera osv. Normalt är högsta antalet sådana processer baserat på antalet CPU-kärnor. Du kan kontrollera det med den här inställningen.

--max-log-size

Största loggfilstorlek (i MB). Största storlek på loggfiler som skapas av servern. När loggfilen blir större än denna storlek roteras den automatiskt. Satt till noll för att inaktivera loggrotation.

--max-opds-items

Högst antal böcker i OPDS-flöden. Högsta antalet böcker som servern kommer att returnera i ett enskilt OPDS-anskaffningsflöde.

--max-opds-ungrouped-items

Högst antal ogrupperade poster i OPDS-flöden. Gruppera poster i kategorier som författare/taggar efter första bokstaven när det finns fler än det här antalet poster. Ange noll för att inaktivera.

--max-request-body-size

Största tillåtna storlek för filer som överförs till servern (i MB).

--num-per-page

Antal böcker att visa på en enskild sida. Antalet böcker som ska visas på en enskild sida i webbläsaren.

--pidfile

Skriv process-PID till angiven fil

--port

Port att lyssna på efter anslutningar.

--search-the-net-urls

Sökväg till en JSON-fil som innehåller URL:er för funktionen "Sök på internet". Det enklaste sättet att skapa en sådan fil är att gå till Inställningar->Dela via nätet->Sök på internet i calibre, skapa URL:erna och exportera dem.

--shutdown-timeout

Total tid i sekunder att vänta på ren avstängning.

--ssl-certfile

Sökväg till SSL-certifikatfil.

--ssl-keyfile

Sökväg till privat SSL-nyckelfil.

--timeout

Tid (i sekunder) varefter en inaktiv anslutning stängs.

--trusted-ips

Tillåt icke-autentiserade anslutningar från specifika IP-adresser att göra ändringar. Normalt, om du inte aktiverar autentisering, fungerar servern i skrivskyddat läge för att inte tillåta anonyma användare att göra ändringar i dina calibre-bibliotek. Med det här alternativet kan alla som ansluter från de angivna IP-adresserna göra ändringar. Måste vara en kommaseparerad lista med adress- eller nätverksspecifikationer. Detta är användbart om du vill köra servern utan autentisering men ändå använder calibredb för att göra ändringar i dina calibre-bibliotek. Observera att om du aktiverar det här alternativet betyder det att alla som ansluter från de angivna IP-adresserna kan göra ändringar i dina calibre-bibliotek.

--url-prefix

Ett prefix att lägga till i början på alla URL:er. Användbart om du vill köra den här servern bakom en omvänd proxy. Använd till exempel /calibre som URL-prefix.

--userdb

Sökväg till användardatabasen att använda för autentisering. Databasen är en SQLite-fil. För att skapa den använder du --manage-users. Du kan läsa mer om hantering av användare på: https://manual.calibre-ebook.com/sv/server.html#managing-user-accounts-from-the-command-line-only

--version

visar programmets versionsnummer och avsluta

--worker-count

Antal arbetstrådar som används för att behandla förfrågningar.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibre-smtp

calibre-smtp

calibre-smtp [options] [from to text]

Sända e-post med SMTP-protokollet. calibre-smtp har två utförandelägen. I skrivläget anger du från till och text och dessa används för att skapa och sända ett e-postmeddelande.
I filterläget läser calibre-smtp ett komplett e-postmeddelande från STDIN och sänder det.

text är brödtexten i e-postmeddelandet.
Om text inte anges läses ett komplett e-postmeddelande från STDIN.
från är avsändarens e-postadress och till är mottagarens e-postadress. När ett komplett e-postmeddelande läses från STDIN, används från och till endast i SMTP-förhandlingen, meddelanderubrikerna ändras inte.

När du skickar argument till calibre-smtp som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--fork, -f

Dela upp och leverera meddelande i bakgrunden. Om du använder det här alternativet bör du också använda --outbox för hantera misslyckade leveranser.

--help, -h

visa denna hjälptext och avsluta

--localhost, -l

Värdnamn av lokalvärden. Används vid anslutning till SMTP-server.

--outbox, -o

Sökväg till mappen maildir för att lagra misslyckade leveranser av postmeddelanden i.

--timeout, -t

Tidsbegränsning för förbindelse

--verbose, -v

Var mer informativ

--version

visar programmets versionsnummer och avsluta

COMPOSE MAIL

Alternativ för att komponera e-post. Ignoreras om ingen text anges

--attachment, -a

Fil att bifoga till e-posten

--subject, -s

Ämnesrad för e-posten

SMTP RELAY

Alternativ vid användning av en SMTP-reläserver för att skicka post. calibre kommer försöka skicka direkt om inte –relay anges.

--cafile

Sökväg till en fil med sammanförda CA-certifikat i PEM-format som används för att verifiera servercertifikatet när du använder TLS. Systemets CA-certifikat används som standard.

--dont-verify-server-certificate

Verifiera inte serverns certifikat när du ansluter med TLS. Detta brukade vara standardbeteendet i calibre versioner före 3.27. Om du använder en relä med ett självsignerat eller på annat sätt ogiltigt certifikat, kan du använda det här alternativet för att återställa beteendet före 3.27

--encryption-method, -e

Krypteringsmetod att använda vid anslutning till reläserver. Valen är TLS, SSL och INGEN. Standard är TLS. VARNING: Att välja INGEN är mycket osäkert

--password, -p

Lösenord för reläserver

--port

Port att ansluta till reläserver. Standard är att använda 465 om krypteringsmetod är SSL och annars 25.

--relay, -r

En SMTP-reläserver för att skicka post.

--username, -u

Användarnamn för reläserver

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- calibredb

calibredb

calibredb command [options] [arguments]

calibredb är kommandoradsgränssnittet till calibre-databasen. Det har flera underkommandon, dokumenterade nedan.

calibredb kan användas för att manipulera antingen en calibre-databas anges per sökväg eller en calibre-innehållsserver körs antingen på den lokala maskinen eller över internet. Du kan starta en calibre-Innehållsserver med antingen calibre-server programmet eller klicka på i calibre-huvudprogrammet Anslut/dela  →
Starta innehållsserver. Eftersom calibredb kan göra ändringar i ditt calibre-bibliotek måste du först ställa in autentisering på servern. Det finns två sätt att göra det:

- Om du planerar att bara ansluta till en server som körs på samma dator, kan du helt enkelt använda flaggan --enable-local-write av innehållsservern, så att alla program, inklusive calibredb, körs på den lokala datorn för att göra ändringar i dina calibre-data. När du kör servern från calibre-huvudprogrammet är det här alternativet i Inställningar → Dela via nätet → Avancerat.

- Om du vill aktivera åtkomst via internet bör du ställa in användarkonton på servern och använda --username eller --password flaggorna för calibredb för att ge den åtkomst. Du kan ställa in användarautentisering för calibre server genom att använda --enable-auth flaggan och använda --manage-users för att skapa användarkonton.
Om du kör servern från calibre-huvudprogrammet använder du Inställningar → Dela via nätet → Kräv användarnamn/lösenord.

För att ansluta till en körande innehållsserver, skicka server-URL:en med flaggan --with-library, se dokumentationen för den flaggan för detaljer och exempel.

- GLOBALA ALTERNATIV

- list

- add

- TILLÄGGNING TILL FRÅN MAPPAR

- remove

- add_format

- remove_format

- show_metadata

- set_metadata

- export

- catalog

- EPUB ALTERNATIV

- 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

GLOBALA ALTERNATIV

--help, -h

visa denna hjälptext och avsluta

--library-path, --with-library

Sökväg till calibre-biblioteket. Standard är att använda sökvägen lagrad i inställningarna. Du kan också ansluta till en calibre-innehållsserver för att utföra åtgärder på fjärrbibliotek. Så här använder du en URL av formen: http://hostname:port/#bibliotek_id till exempel http://localhost:8080/#mittbibliotek. Bibliotek_id är bibliotekets identifierare för det bibliotek du vill ansluta till på innehållsservern. Du kan använda det speciella bibliotek_id-värdet för att få en lista över biblioteksidentifierare tillgängliga på servern. Mer information om hur du anpassar åtkomst via en innehållsserver finns i https://manual.calibre-ebook.com/sv/generated/sv/calibredb.html.

--password

Lösenord för anslutning till en calibre-innehållsserver. För att läsa lösenordet från standardinmatning, använd specialvärdet: <stdin>. För att läsa lösenordet från en fil, använd: <f:/path/to/file> (dvs. <f: följt av hela sökvägen till filen och en efterföljande>). Vinkelparenteserna ovan är nödvändiga, kom ihåg att undkomma dem eller använd citattecken för ditt skal.

--timeout

Tidsgränsen, i sekunder, när du ansluter till ett calibre-bibliotek via nätverket. Standard är två minuter.

--username

Användarnamn för anslutning till en calibre-innehållsserver

--version

visar programmets versionsnummer och avsluta

list

calibredb list [options]

Lista de böcker som finns i calibre-databasen.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--ascending

Sortera resultaten i stigande ordning

--fields, -f

Fälten som ska visas när böcker listar i databasen. Bör vara en kommaseparerad lista med fält. Tillgängliga fält: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Standard: title,authors. Specialfältet "alla" kan användas för att välja alla fält. Förutom de inbyggda fälten ovan är anpassade fält också tillgängliga som *field_name, till exempel för ett anpassat fält #rating, använd namnet: *rating

--for-machine

Skapa utmatning i JSON-format, vilket är mer lämpat för maskintolkning. Resulterar i radbredd och avgränsningsalternativ att ignorera.

--limit

Högsta antalet resultat att visa. Standard: alla

--line-width, -w

Största bredd på en enskild rad i utmatning. Standardinställningen är att upptäcka skärmstorlek.

--prefix

Prefixet för alla sökvägar. Standard är den absoluta sökvägen till biblioteksmappen.

--search, -s

Filtrera resultat efter sökförfrågan. För information om sökformatet, se sökrelaterad dokumentation i användarmanualen. Standard är att inte filtrera.

--separator

Sträng som används för att separera fält. Standard är blanksteg.

--sort-by

Fältet efter vilket resultaten ska sorteras. Du kan ange flera fält genom att separera dem med kommatecken. Tillgängliga fält: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Standard: id

--template

Mallen som ska köras om "template" finns i fältlistan. Standard: Ingen

--template_file, -t

Sökväg till en fil som innehåller mallen som ska köras om "template" finns i fältlistan. Standard: Ingen

--template_heading

Rubrik till mallkolumnen. Standard: template. Det här alternativet ignoreras om alternativet --for-machine är inställt

add

calibredb add [options] file1 file2 file3 ...

Lägg till de angivna filerna som böcker i databasen. Du kan även ange mappar, se mapprelaterade alternativ nedan.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--authors, -a

Ange författarna för den tillagda boken eller de tillagda böckerna

--automerge, -m

Om böcker med liknande titlar och författare hittas, slå samman de inkommande formaten (filer) automatiskt med befintliga bokposter. Värdet "ignore" betyder att dubblettformat kasseras. Värdet "overwrite" betyder att dubblettformat i biblioteket skrivs över med de nyligen tillagda filerna. Värdet "new_record" betyder att dubblettformat placeras i en ny bokpost.

--cover, -c

Sökväg till omslaget att använda för den tillagda boken

--duplicates, -d

Lägg till böcker i databasen även om de redan finns. Jämförelse görs baserat på boktitlar och författare. Observera att alternativet --automerge har företräde.

--empty, -e

Lägg till en tom bok (en bok utan format)

--identifier, -I

Ange identifierare för den här boken, t.ex. -I asin:XXX -I isbn:YYY

--isbn, -i

Ange ISBN för den tillagda boken eller de tillagda böckerna

--languages, -l

En kommaseparerad lista med språk (bäst att använda ISO639-språkkoder, även om vissa språknamn kanske också känns igen)

--series, -s

Ange serier för den tillagda boken eller de tillagda böckerna

--series-index, -S

Ange serienummer för den tillagda boken eller de tillagda böckerna

--tags, -T

Ange taggar i den tillagda boken eller de tillagda böckerna

--title, -t

Ange namn på den tillagda boken eller de tillagda böckerna

TILLÄGGNING TILL FRÅN MAPPAR

Alternativ för att anpassa tilläggning av böcker från mappar. Som standard läggs endast filer som har ändelser av kända e-boksfiltyper till.

--add

Ett filnamnsmönster (glob), filer som matchar det här mönstret kommer att läggas till vid skanning av mappar efter filer, även om de inte är av en känd e-boksfiltyp. Kan anges flera gånger för flera mönster.

--ignore

Ett filnamn (glob)-mönster, filer som matchar det här mönstret ignoreras vid skanning av mappar efter filer. Kan anges flera gånger för flera mönster. Till exempel: *.pdf ignorerar alla PDF-filer

--one-book-per-directory, -1

Antag att varje mapp endast har en enda logisk bok och att alla filer i den är olika e-boksformat av den boken

--recurse, -r

Bearbeta mappar rekursivt

remove

calibredb remove ids

Ta bort böcker som identifierats av id från databasen. id:et ska vara en kommaseparerad lista med id-nummer (du kan få id-nummer med sökkommandot). Till exempel 23,34,57-85 (när du anger ett intervall, det sista numret i intervallet inkluderas inte).

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--permanent

Använd inte Papperskorgen

add_format

calibredb add_format [options] id ebook_file

Lägg e-boken i ebook_file till tillgängliga format för den logiska boken som identifieras av id. Du kan få id genom att använda sökkommandot. Om formatet redan finns ersätts det, såvida inte ersätt inte flaggan anges.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--as-extra-data-file

Lägg till filen som en extra datafil till boken, inte som ett e-boksformat

--dont-replace

Ersätt inte formatet om det redan finns

remove_format

calibredb remove_format [options] id fmt

Ta bort formatet fmt från den logiska boken identifierad med hjälp av id. Du kan få id med sökkommandot. fmt bör vara en filändelse som LRF eller TXT eller EPUB. Om den logiska boken inte har fmt tillgängligt, görs inget.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

show_metadata

calibredb show_metadata [options] id

Visa metadata lagrade i calibre-databasen för boken identifierad av id.
id är ett id-nummer från sökkommandot.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--as-opf

Skriv metadata i OPF-form (XML)

set_metadata

calibredb set_metadata [options] book_id [/path/to/metadata.opf]

Ställ in metadata som lagras i calibre-databasen för den bok som identifieras med book_id från OPF-filen metadata.opf. book_id är ett bok-id-nummer från sökkommandot. Du kan få en snabb känsla för OPF-formatet genom att använda omkopplaren –as-opf till kommandot show_metadata. Du kan också ställa in metadata för enskilda fält med flaggan –field. Om du använder flaggan –field behöver du inte ange en OPF-fil.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--field, -f

Fältet för att ställa in. Format är field_name:value, till exempel: --field tags:tag1,tag2. Använd --list-fields för att få en lista över alla fältnamn. Du kan ange det här alternativet flera gånger för att ställa in flera fält. Observera: För språk du måste använda ISO639-språkkoder (t.ex. en för engelska, fr för franska och så vidare). För identifierare, är syntaxen --field identifiers:isbn:XXXX,doi:YYYYY. För boolean (ja/nej) fält använder sann och falsk eller ja och nej.

--list-fields, -l

Ange namn på metadatafälten som kan användas med flaggan --field

export

calibredb export [options] ids

Exportera böckerna som anges av identifierare (en kommaseparerad lista) till filsystemet.
Exporten sparar alla format av boken, dess omslag och metadata (i en OPF-fil). Eventuella extra datafiler som är kopplade till boken sparas också.
Du kan få id-nummer från sökkommandot.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--all

Exportera alla böcker i databasen och ignorera listan med identifierare.

--dont-asciiize

Låt calibre konvertera alla icke-engelska tecken till engelska ekvivalenter för filnamn. Detta är användbart om du sparar i ett äldre filsystem utan fullt stöd för Unicode-filnamn. Om denna växel aktiveras, kommer detta beteende att stängas av.

--dont-save-cover

Normalt sparar calibre omslaget i en separat fil tillsammans med själva e-bokfilerna. Om denna växel aktiveras, kommer detta beteende att stängas av.

--dont-save-extra-files

Spara alla datafiler som är kopplade till boken när du sparar boken Om denna växel aktiveras, kommer detta beteende att stängas av.

--dont-update-metadata

Normalt kommer calibre att uppdatera metadata i de sparade filerna utifrån det som finns i calibre-biblioteket. Detta gör att filerna sparas långsammare till disken. Om denna växel aktiveras, kommer detta beteende att stängas av.

--dont-write-opf

Normalt kommer calibre att skriva metadata i en separat OPF-fil tillsammans med själva e-bokfilerna. Om denna växel aktiveras, kommer detta beteende att stängas av.

--formats

Kommaseparerad lista med format att spara för varje bok. Som standard sparas alla tillgängliga format.

--progress

Rapportera förlopp

--replace-whitespace

Ersätt "whitespace" med understreck.

--single-dir

Exportera alla böcker till en enda mapp

--template

Mallen för att anpassa filnamn och mappstruktur för de sparade filerna. Standard är "{author_sort}/{title}/{title} - {authors}" vilket sparar böcker i en undermapp per författare med filnamn bestående av titel och författare. Tillgängliga kontroller är: {author_sort, authors, id, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

Format för datumvisning. %d - dag, %b - månad, %m - månadsnummer, %Y - år. Standard är: %b, %Y

--to-dir

Exportera böcker till den angivna mappen. Standard är .

--to-lowercase

Konvertera sökvägar till gemener.

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.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--ids, -i

Kommaseparerad lista över databas-ID till katalogen. Om de deklareras, ignoreras --search. Standard: alla

--search, -s

Filtrera resultat efter sökförfrågan. För information om sökformatet, se sökrelaterad dokumentation i användarmanualen. Standard: Ingen filtrering

--verbose, -v

Visa detaljerad utmatningsinformation. Användbart för felsökning

EPUB ALTERNATIV

--catalog-title

Titel på den skapade katalogen som används som titel i metadata. Standard: 'My Books' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--cross-reference-authors

Skapa korsreferenser i författarsektionen för böcker med flera författare. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--debug-pipeline

Spara utmatning från olika stadier av konverteringsprocessen till den angivna mappen. Användbart om du är osäker på i vilket skede av konverteringsprocessen ett problem inträffar. Standard: 'None' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--exclude-genre

Regex beskriver taggar som ska exkluderas som genrer Standard: '[.+]|^+$' exkluderar parentestaggar, t.ex. '[Project Gutenberg]', och '+', standard-tagg för lästa böcker. Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--exclusion-rules

Anger de regler som används för att exkludera böcker från den skapade katalogen. Modellen för en undantagsregel är antingen ('<regel namn>','Taggar','<komma-separerad lista med taggar>') eller ('<regel namn>','<anpassad column>','<mönster>'). Till exempel: (("Arkiverade böcker", "# status", "Arkiverade"),) kommer att exkludera en bok med ett värde på "status" "Arkiverat" i den anpassade kolumnen. När flera regler är definierade, kommer alla regler tillämpas. Standard: "(('Catalogs','Tags','Catalog'),)" Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-authors

Inkludera 'Författar' sektion i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-descriptions

Inkludera sektionen 'Beskrivningar' i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-genres

Inkludera sektionen 'Genrer' i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-recently-added

Inkludera sektioen 'Nyligen tillagda' i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-series

Inkludera sektioen 'Serier' i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--generate-titles

Inkludera sektionen 'Titlar' i katalogen. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--genre-source-field

Källfältet för "Genrer" sektion. Standard: 'Taggar' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--header-note-source-field

Anpassat fält innehåller anmärkningstext att infoga i beskrivningsrubrik. Standard: '' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--merge-comments-rule

#<custom field>:[before|after]:[True|False] anger: <custom field> Anpassat fält innehåller anteckningar att slå samman med kommentarer [before|after] Placering av anteckningar med avseende på kommentarer [True|False] - En horisontell linje infogas mellan anteckningar och kommentarer Standard: '::' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--output-profile

Anger utmatningsprofilen. I vissa fall krävs en utmatningsprofil för att optimera katalogen för enheten. Till exempel skapar "kindle" eller "kindle_dx" en strukturerad innehållsförteckning med avsnitt och artiklar. Standard: 'None' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--prefix-rules

Anger de regler som används för att inkludera prefix som indikerar lästa böcker, önskelistsposter och andra användardefinierade prefix. Modellen för en prefixregel är ('<rule name>','<source field>','<pattern>','<prefix>'). När flera regler är definierade, kommer den första matchande regeln användas. Standard: "(('Read books','tags','+','✓'),('Wishlist item','tags','Wishlist','×'))" Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--preset

Använd en namngiven förinställning skapas med GUI Catalog Builder. En förinställning specificerar alla inställningar för att bygga en katalog. Standard: 'None' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--thumb-width

Storleksråd (i tum) för bokomslag i katalog. Intervall: 1.0 - 2.0 Standard: '1.0' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

--use-existing-cover

Ersätt befintliga omslag när du skapar katalog. Standard: 'False' Gäller: AZW3-, EPUB-, MOBI-utmatningsformat

saved_searches

calibredb saved_searches [options] (list|add|remove)

Hantera sparade sökningar i den här databasen.
Om du försöker lägga till en förfrågan med ett namn som redan finns, kommer den att ersättas.

Syntax för att lägga till:

calibredb saved_searches add search_name search_expression

Syntax för att ta bort:

calibredb saved_searches remove search_name

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

add_custom_column

calibredb add_custom_column [options] label name datatype

Skapa en anpassad kolumn. label är det maskinvänliga namnet på kolumnen. Får inte innehålla blanksteg eller kolon. name är det människovänliga namnet på kolumnen.
datatype är en av: bool, comments, composite, datetime, enumeration, float, int, rating, series, text

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--display

En ordbok med alternativ för att anpassa hur data i den här kolumnen ska tolkas. Det här är en JSON-sträng. För uppräkningskolumner, använd --display"{\ "enum_values\ ":[\ "val1\ ", \ "val2\ "]}" Det finns många alternativ som kan gå in i visningsvariabel. Alternativen per kolumntyp är: 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 Det bästa sättet att hitta giltiga kombinationer är att skapa en anpassad kolumn av lämplig typ i gränssnittet och sedan titta på OPF-säkerhetskopian för en bok (se till att en ny OPF har skapats sedan kolumnen lades till). Du kommer att se JSON för "visningen" av den nya kolumnen i OPF.

--is-multiple

Denna kolumn lagrar taggar som data (dvs. flera kommaseparerade värden). Gäller endast om datatypen är text.

custom_columns

calibredb custom_columns [options]

Lista anpassade kolumner. Visar kolumnnamn och identifierare.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--details, -d

Visa detaljer för varje kolumn.

remove_custom_column

calibredb remove_custom_column [options] label

Ta bort den anpassade kolumnen som identifieras med etikett. Du kan se tillgängliga kolumner med kommandot custom_columns.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--force, -f

Fråga inte efter bekräftelse

set_custom

calibredb set_custom [options] column id value

Ställ in värdet på en anpassad kolumn för boken som identifieras av identifierare.
Du kan få en lista med id med hjälp av sökkommandot.
Du kan få en lista med anpassade kolumnnamn med hjälp av custom_columns kommando.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--append, -a

Om kolumnen lagrar flera värden, lägg till de angivna värdena till de befintliga i stället för att ersätta dem.

restore_database

calibredb restore_database [options]

Återställ denna databas från metadata som lagras i OPF-filer i varje mapp i calibre-biblioteket. Det här är användbart om din metadata.db-fil har skadats.

VARNING: Det här kommandot återskapar din databas hlet. Du förlorar alla sparade sökningar, användarkategorier, pluggbrädor, lagrade konverterings inställningar per bok och anpassade recept. Återställda metadata är bara så exakta som det som finns i OPF-filerna.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--really-do-it, -r

Verkligen gör återställningen. Kommandot kommer inte köras om det här alternativet har angetts.

check_library

calibredb check_library [options]

Utföra vissa kontroller av filsystemet som representerar ett bibliotek. Rapporter är invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--csv, -c

Utmatning i CSV

--ignore_extensions, -e

Kommaseparerad lista över tillägg att ignorera. Standard: alla

--ignore_names, -n

Kommaseparerad lista över namn att ignorera. Standard: Alla

--report, -r

Kommaseparerad lista över rapporter. Standard: Alla

--vacuum-fts-db

Rensa heltextsökningsdatabasen. Detta kan vara mycket långsamt och minneskrävande, beroende på databasens storlek.

list_categories

calibredb list_categories [options]

Skapa en rapport om kategoriinformationen i databasen.
Informationen motsvarar vad som visas i taggbläddraren.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--categories, -r

Kommaseparerad lista över kategoriuppslagsnamn. Standard: Alla

--csv, -c

Utmatning i CSV

--dialect

Den typ av CSV-fil som ska produceras. Val: excel, excel-tab, unix

--item_count, -i

Mata endast ut antalet poster i en kategori istället för antalet per post inom kategorin

--width, -w

Största bredd på en enskild rad i utmatning. Standardinställningen är att upptäcka skärmstorlek.

backup_metadata

calibredb backup_metadata [options]

Säkerhetskopiera metadata som lagras i databasen inuti individuella OPF-filer i varje bokmapp. Det här sker normalt automatiskt, men du kan köra det här kommando för att tvinga nyskapande av OPF-filer med flaggan –all.

Observera att det är normalt ingen anledning att göra det här, eftersom OPF-filerna säkerhetskopieras automatiskt varje gång metadata ändras.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--all

Normalt fungerar det här kommandot endast på böcker som har inaktuella OPF-filer. Det här alternativet gör att det fungerar på alla böcker.

clone

calibredb clone sökväg/till/nytt/bibliotek

Skapa en klon av det aktuella biblioteket. Det här skapar ett nytt, tomt bibliotek som har alla samma anpassade kolumner, virtuella bibliotek och andra inställningar som det aktuella biblioteket.

Det klonade biblioteket kommer inte att innehålla några böcker. Om du vill skapa en fullständig dubblett, inklusive alla böcker, använd helt enkelt dina filsystemverktyg för att kopiera biblioteksmappen.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

embed_metadata

calibredb embed_metadata [options] book_id

Uppdatera metadata i själva bokfilerna som är lagrade i calibre-biblioteket från metadata i calibre-databasen. Normalt uppdateras metadata endast vid export av filer från calibre, det här kommandot är användbart om du vill att filerna ska uppdateras på plats. Observera att olika filformat stöder olika mängder metadata. Du kan använda specialvärdet ’all’ för book_id för att uppdatera metadata i alla böcker. Du kan också ange många bokidentifierare åtskilda med blanksteg och id-intervall åtskilda med bindestreck. Till exempel: calibredb embed_metadata 1 2 10-15 23

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--only-formats, -f

Uppdatera bara metadata i filer för ett särskilt format. Specificera det flera gånger för flera format. Som standard kommer alla format uppdateras.

search

calibredb search [options] sökuttryck

Sök i biblioteket efter den angivna söktermen och returnera en kommaseparerad lista med bokidentifierare som matchar sökuttrycket. Utmatningsformatet är användbart för att mata in andra kommandon som accepterar en lista med identifierare som inmatning.

Sökuttrycket kan vara vad som helst från calibres kraftfulla sökfrågespråk, till exempel: calibredb search author:asimov ’title:”i robot”’

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--limit, -l

Högsta antalet resultat att returnera. Standard är alla resultat.

fts_index

calibredb fts_index [options] enable/disable/status/reindex

Kontrollera indexeringsprocessen för heltextsökning.

enable

Aktiverar FTS-indexering för det här biblioteket

disable

Inaktiverar av FTS-indexering för det här biblioteket

status

Visar aktuell indexeringsstatus

reindex

Kan användas för att återindexera antingen vissa böcker eller hela biblioteket. För att återindexera vissa böcker, specificera bok-id:et som ytterligare argument efter kommandot reindex. Om inga bokidentifierare anges återindexeras hela biblioteket.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--indexing-speed

Indexeringshastigheten. Använd snabb för snabb indexering med alla dina datorresurser och långsam för mindre resurskrävande indexering. Observera att hastigheten återställs till långsam efter varje anrop.

--wait-for-completion

Vänta tills alla böcker har indexerats och visar indexeringsförlopp med jämna mellanrum

fts_search

calibredb fts_search [options] sökuttryck

Gör en heltextsökning på hela biblioteket eller en delmängd av det.

När du skickar argument till calibredb som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

--do-not-match-on-related-words

Matcha endast på exakta ord, inte relaterade ord. Så korrigering kommer inte att matcha korrigering.

--include-snippets

Inkludera kodavsnitt som omger varje matchning. Observera att detta gör sökningen mycket långsammare.

--indexing-threshold

Hur mycket av biblioteket som måste indexeras innan sökning är tillåten, i procent. Standard är 90

--match-end-marker

Markören som används för att indikera slutet på ett matchat ord i ett kodavsnitt

--match-start-marker

Markören som används för att indikera början på ett matchat ord i ett kodavsnitt

--output-format

Formatet att mata ut sökresultaten i. Antingen "text" för vanlig text eller "json" för JSON-utmatning.

--restrict-to

Begränsa de sökta böckerna, antingen med ett sökuttryck eller identifierare. Till exempel: ids:1,2,3 för att begränsa med id eller search:tag:foo för att begränsa till böcker med taggen foo.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- ebook-convert

ebook-convert

ebook-convert input_file output_file [alternativ]

Konvertera en e-bok från ett format till ett annat.

input_file är inmatningsfilen och output_file är utmatningsfilen. Båda måste anges som de två första argumenten i kommandot.

Utmatningsformatet för e-boken uppskattas utifrån filändelsen i output_file. output_file kan även ges specialformatet .EXT, där EXT är utmatningsfilsändelsen. Om det används kommer namnet på utmatningsfilen att härledas från namnet på inmatningsfilen. Observera att filnamnen inte får börja med bindestreck. Om ingen filinsticksmodul har angivits för utmatningsfilen, så behandlas den som en mapp till vilken en ”Öppen e-bok” (OEB) bestående av HTML filer kommer att skrivas till den mappen. Dessa filer är de filer som normalt sett skulle ha skickats till utmatningsinsticksmodulen.

Efter att ha angivit in- och utmatningsfilerna kan du anpassa konverteringen genom att ange olika alternativ. Vilka alternativ som är tillgängliga beror på in- och utmatningsfiltyperna. För att få hjälp med dem, ange in- och utmatningsfilerna och lägg därefter till alternativet -h.

För fullständig dokumentation om konverteringssystemet, gå till Konvertering av e-bok

När du skickar argument till ebook-convert som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

Alternativen och standardvärden för ändringsalternativ beror på både inmatning- och utmatningsformat, därför bör du alltid kontrollera med:

ebook-convert myfile.input_format myfile.output_format -h

Nedan finns alternativen som är vanliga vid all konvertering följt av alternativen unika för varje inmatning- och utmatningsformat.

- ALTERNATIV FÖR INMATNING

- ALTERNATIV FÖR UTMATNING

- UTSEENDE OCH KÄNSLA

- HEURISTISK BEARBETNING

- SÖK OCH ERSÄTT

- STRUKTURDETEKTERING

- INNEHÅLLSFÖRTECKNING

- METADATA

- FELSÖK

--help, -h

visa denna hjälptext och avsluta

--input-profile

Ange inmatningsprofilen. Inmatningsprofilen ger information till konverteringssystemet om hur uppgifterna i inmatningsdokumentet ska tolkas. Till exempel upplösningsberoende längder (dvs. längder i pixlar). Alternativen är: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

Lista inbyggda receptnamn. Du kan skapa en e-bok från ett inbyggt recept så här: ebook-convert "Receptnamn.recipe" utmatning.epub

--output-profile

Ange utdataprofil. Utdataprofilen anger omvandlingen hur systemet kan optimera skapade dokumentet för den angivna enheten. I vissa fall kan en utgång profil kan användas för att optimera resultatet för en viss enhet, men det är sällan nödvändigt. Alternativen är: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

visar programmets versionsnummer och avsluta

ALTERNATIV FÖR INMATNING

Alternativ för att anpassa bearbetningen av ingående mobi fil

--input-encoding

Ange teckenkodning för inmatningsdokumentet. Det här alternativet åsidosätter eventuell teckenkodning som dokumentet själv anger. Särskilt användbart för dokument som inte deklarerar en kodning eller som har felaktiga kodningsdeklarationer.

ALTERNATIV FÖR UTMATNING

Alternativ för att anpassa bearbetningen av utmatning epub

--dont-split-on-page-breaks

Inaktivera delning vid sidbrytningar. Normalt delas inmatningsfilen automatiskt vid varje sidbrytning i två filer. Detta ger en utmatad e-bok som kan analyseras snabbare och med mindre resurser. Att dela är dock långsamt och om din källfil innehåller ett mycket stort antal sidbrytningar bör du inaktivera delning vid sidbrytning.

--epub-flatten

Det här alternativet behövs endast om du tänker använda EPUB:en med FBReaderJ. Det plattar ut filsystemet inuti EPUB:en och placerar alla filer i den översta nivån.

--epub-inline-toc

Infoga en indragsinnehållsförteckning som kommer att visas som en del av bokens innehåll.

--epub-max-image-size

Maximal bildstorlek (bredd x höjd). Ett värde på none betyder att du använder skärmstorleken från utmatningsprofilen. Ett värde på profile betyder att ingen maximal storlek har angetts. Till exempel kommer ett värde på 100x200 att göra att alla bilder ändras så att deras bredd inte är mer än 100 pixlar och deras höjd inte är mer än 200 pixlar. Observera att detta endast påverkar storleken på själva bildfilerna. Vilken bild som helst kan renderas i en annan storlek beroende på formgivningen som tillämpas på den i dokumentet.

--epub-toc-at-end

Lägg till den infogade indragsinnehållsförteckningen i slutet av boken istället för i början.

--epub-version

Versionen av EPUB-filen som ska skapas. EPUB 2 är den mest kompatibla, använd endast EPUB 3 om du vet att du faktiskt behöver det.

--extract-to

Extrahera innehållet i den skapade EPUB-filen till den angivna mappen. Innehållet i mappen tas bort först, så var försiktig.

--flow-size

Dela alla HTML-filer som är större än denna storlek (i KiB). Detta är nödvändigt eftersom de flesta EPUB-läsenheter inte kan hantera stora filer. Standardvärdet 260KB är den storlek som krävs för Adobe Digital Editions. Satt till 0 för stänga av storleksbaserad delning.

--no-default-epub-cover

Om inmatningsfilen inte har något omslag och du inte ställer in ett, skapas normalt ett standardomslag skapats med titel, författare, osv. Med det här alternativet aktiverat skapas inga standardomslag.

--no-svg-cover

Använd inte SVG-grafik till omslag. Använd om din EPUB-fil kommer att användas på enheter som inte stöder SVG, t.ex. iPhone och JetBook Lite. Om det här alternativet inte väljs, kommer dessa enheter att visa ett tomt omslag.

--preserve-cover-aspect-ratio

När man använder ett SVG-omslag kommer det här alternativet att få omslaget att skalas om för att täcka det tillgängliga skärmområdet, men bevarar fortfarande dess bildförhållande (förhållandet mellan bredd och höjd). Det innebär att det kan finnas vita kanter på sidorna eller på toppen och botten av bilden, men bilden kommer aldrig att förvrängas. Om det här alternativet inaktiveras, kan bilden bli en aning förvrängd, men det kommer inte finnas några ramar.

--pretty-print

Om det anges, kommer utmatningsinsticksmodulen att försöka skapa utmatning som är så mänskligt läsbar som möjligt. Det här kanske inte har någon effekt för en del utmatningsinsticksmoduler.

--toc-title

Titel för alla skapade infogade innehållsförteckningar.

UTSEENDE OCH KÄNSLA

Alternativ för att anpassa utmatningens utseende och känsla

--asciiize

Translitterera Unicode-tecken till en ASCII-representation. Används med försiktighet, eftersom detta kommer att ersätta Unicode-tecken med ASCII. Det kommer till exempel att ersätta "Pelé" med "Pele". Observera också att i fall där det finns flera representationer av ett tecken (exempelvis tecken som delas av kinesiska och japanska) kommer representationen baserad på det aktuella calibre-gränssnittsspråket att användas.

--base-font-size

Grundteckensnittsstorleken i punkter. Alla teckensnittsstorlekar i den producerade boken kommer att skalas om baserat på den här storleken. Genom att välja en större storlek kan du få teckensnittet i utmatningen större och vice versa. Som standard, när värdet är noll kommer grundteckensnittsstorleken för teckensnitt att väljas baserat på utmatningsprofilen du väljer.

--change-justification

Ändra textjusteringen. Värdet "vänster" konverterar all marginaljusterad text i källan till vänsterjusterad text (dvs. ojusterad). Med värdet "justera" konverteras all ojusterad text till mariginaljusterad. Värdet "original" (standard) behåller de inställningar för justering som anges i källfilen. Observera att endast vissa format stöder mariginaljustering.

--disable-font-rescaling

Inaktivera all omskalning av teckensnittsstorlekar.

--embed-all-fonts

Bädda in varje teckensnitt som refereras i inmatningsdokumentet som inte redan är inbäddat. Detta kommer att söka i ditt system efter teckensnitt och om de påträffas, kommer de att bäddas in. Inbäddning fungerar bara om det format du konverterar till stöder inbäddade teckensnitt, t.ex. EPUB, AZW3, DOCX eller PDF. Se till att du har rätt licens för att bädda in teckensnitt som används i detta dokument.

--embed-font-family

Bädda in den angivna teckensnittsfamiljen i boken. Här anges "bas"-teckensnitt som används för boken. Om inmatningsdokumentet specificerar sina egna teckensnitt, kan de åsidosätta detta grundteckensnitt. Du kan använda informationsalternativet filterformat för att ta bort teckensnitt från inmatningsdokumentet. Observera att bädda in teckensnitt endast fungerar med vissa utmatningsformat, främst EPUB, AZW3 och DOCX.

--expand-css

Som standard kommer calibre att använda stenografiformen för olika CSS-egenskaper som marginal, utfyllnad, kanter, osv. Det här alternativet kommer att få den att använda hela expanderade formen istället. Observera att CSS alltid utökas vid skapande av EPUB-filer med utmatningsprofilen inställd på en av Nook-profilerna eftersom Nook inte kan hantera stenografisk CSS.

--extra-css

Antingen sökvägen till ett CSS-formatmall eller rå CSS. Denna CSS läggs till i formatreglerna från källfilen, så den kan användas för att åsidosätta dessa regler.

--filter-css

En kommaseparerad lista över CSS-egenskaper som kommer att tas bort från alla CSS-formatregler. Detta är användbart om förekomsten av viss formatinformation förhindrar att den åsidosätts på din enhet. Till exempel: font-family,color,margin-left,margin-right

--font-size-mapping

Mappning från CSS-teckensnittsnamn till teckensnittsstorlekar i punkter. En exempelinställning är 12,12,14,16,18,20,22,24. Detta konverterar storlekarna xx-liten till xx-stor, den sista storleken används för enorma teckensnitt. Omskalningsalgoritmen använder dessa storlekar för att på ett smart sätt skala om teckensnitten. Som standard används en kartläggning baserad på din valda utmatningsprofil.

--insert-blank-line

Infoga en tom rad mellan stycken. Fungerar inte om källfilen inte använder stycken (<p>- eller <div>-taggar).

--insert-blank-line-size

Ställ in höjden på de infogade tomma raderna (i em). Höjden på raderna mellan styckena kommer att vara det dubbla av det här värdet.

--keep-ligatures

Bevara ligaturer som finns i inmatningsdokumentet. En ligatur är en speciell rendering av ett teckenpar som ff, fi, fl och så vidare. De flesta läsenheter saknar stöd för ligaturer i deras standardteckensnitt så det är osannolikt att de renderas korrekt. Som standard konverterar calibre en ligatur till motsvarande par av normala tecken. Det här alternativet kommer att bevara ligaturerna istället.

--line-height

Radavståndet i punkter. Anpassar avstånd mellan på varandra följande textrader. Gäller endast element som inte definierar sitt eget radavstånd. I de flesta fall är det minsta radavståndet valet mer användbart. Som standard utför ingen ändring i radavstånd.

--linearize-tables

Vissa dåligt utformade dokument använder tabeller för att anpassa textutformningen för texten på sidan. Vid konvertering har dessa dokument ofta text som går utanför sidan och andra artefakter. Det här alternativet extraherar innehållet från tabellerna och presenterar det linjärt.

--margin-bottom

Ställ in nedre marginalen i punkter. Standard är 5.0. Om du ställer in det här till mindre än noll kommer ingen marginal att ställas in (marginalinställningen i originaldokumentet bevaras). Observera: Sidorienterade format som PDF och DOCX har egna marginalinställningar som har företräde.

--margin-left

Ställ in vänstra marginalen i punkter. Standard är 5.0. Om du ställer in det här till mindre än noll kommer ingen marginal att ställas in (marginalinställningen i originaldokumentet bevaras). Observera: Sidorienterade format som PDF och DOCX har egna marginalinställningar som har företräde.

--margin-right

Ställ in högra marginalen i punkter. Standard är 5.0. Om du ställer in det här till mindre än noll kommer ingen marginal att ställas in (marginalinställningen i originaldokumentet bevaras). Observera: Sidorienterade format som PDF och DOCX har egna marginalinställningar som har företräde.

--margin-top

Ställ in övre marginalen i punkter. Standard är 5.0. Om du ställer in det här till mindre än noll kommer ingen marginal att ställas in (marginalinställningen i originaldokumentet bevaras). Observera: Sidorienterade format som PDF och DOCX har egna marginalinställningar som har företräde.

--minimum-line-height

Minsta radavståndet, i procent av elementets beräknade teckensnittsstorlek. calibre kommer att säkerställa att varje element har ett radavstånd på åtminstone den här inställningen, oavsett vad ingångsdokumentet specificerar. Ställ in till noll för att inaktivera. Standard är 120%. Använd den här inställningen i stället för det direkt angivna radavståndet, om du inte vet vad du gör. Till exempel kan du uppnå "dubbel radavstånd" i texten genom att ställa in det här till 240.

--remove-paragraph-spacing

Ta bort avstånd mellan stycken. Drar även in första raden på det nya stycket med 1,5 em. Borttagning av avstånd fungerar inte om källfilen inte använder stycken (<p>- eller <div>-taggar).

--remove-paragraph-spacing-indent-size

När calibre tar bort tomma rader mellan stycken, anger det automatiskt ett styckeindrag, för att se till att styckeindelningen syns tydligt. Det här alternativet bestämmer bredden för indraget (i em). Om du anger ett negativt värde kommer indraget som anges i inmatningsdokumentet användas, det vill säga, calibre ändrar inte indraget.

--smarten-punctuation

Konvertera rena citattecken, bindestreck och ellipser till deras typografiskt korrekta motsvarigheter. För detaljer, se https://daringfireball.net/projects/smartypants.

--subset-embedded-fonts

Använd delmängd av alla inbäddade teckensnitt. Varje inbäddat teckensnitt reduceras till endast innehålla de glyfer som används i detta dokument. Detta minskar storleken på teckensnittsfiler. Användbart om du bäddar in ett särskilt stort teckensnitt med massor av oanvända glyfer.

--transform-css-rules

Sökväg till en fil som innehåller regler för att omvandla CSS-format i den här boken. Det enklaste sättet att skapa en sådan fil är att använda guiden för att skapa regler i det grafiska gränssnittet för calibre. Gå till det i avsnittet "Utseende & känsla->Omvandla format" i konverteringsdialogrutan. När du har skapat reglerna kan du använda knappen "Exportera" för att spara dem till en fil.

--transform-html-rules

Sökväg till en fil som innehåller regler för att omvandla HTML i den här boken. Det enklaste sättet att skapa en sådan fil är att använda guiden för att skapa regler i det grafiska gränssnittet för calibre. Öppna den i avsnittet "Utseende & känsla->Omvandla HTML" i konverteringsdialogrutan. När du har skapat reglerna kan du använda knappen "Exportera" för att spara dem i en fil.

--unsmarten-punctuation

Konvertera tjusiga citattecken, streck och ellipser till deras vanliga motsvarigheter.

HEURISTISK BEARBETNING

Ändra dokumenttexten och strukturen med vanliga mönster. Inaktiverad som standard. Använd –enable-heuristics för att aktivera. Individuella åtgärder kan inaktiveras med alternativen –disable-*.

--disable-dehyphenate

Analysera avstavade ord i hela dokumentet. Själva dokumentet används som en ordbok för att avgöra om bindestreck ska behållas eller tas bort.

--disable-delete-blank-paragraphs

Ta bort tomma stycken från dokumentet när de förekommer mellan vartannat stycke

--disable-fix-indents

Vändningsindrag som skapats från flera icke-brytande blankstegsentiteter i CSS-indrag.

--disable-format-scene-breaks

Vänsterjusterade scenbrytningsmarkörer är centrerade. Ersätt mjuka scenbrytningar som använder flera tomma rader med horisontella regler.

--disable-italicize-common-cases

Leta efter vanliga ord och mönster som betecknar kursivt format och kursiverar dem.

--disable-markup-chapter-headings

Identifiera oformaterade huvud- och underrubriker. Ändra dem till H2- och H3-taggar. Den här inställningen kommer inte att skapa en innehållsförteckning, men kan användas i kombination med strukturidentifiering för att skapa ett.

--disable-renumber-headings

Letar efter förekomster av sekventiella <h1>- eller <h2>-taggar. Taggarna numreras om för att förhindra uppdelning i mitten av kapitelrubrikerna.

--disable-unwrap-lines

Radbrytning genom att använda skiljetecken och andra formateringsindikationer.

--enable-heuristics

Aktivera heuristisk bearbetning. Det här alternativet måste ställas in för att någon heuristisk bearbetning ska kunna äga rum.

--html-unwrap-factor

Skala som används för att bestämma längden på vilken en rad ska radbrytas. Giltiga värden är ett decimaltal mellan 0 och 1. Standard är 0,4, precis under medianradens längd. Om bara ett fåtal rader i dokumentet kräver radbrytning bör detta värde minskas

--replace-scene-breaks

Ersätt scenbrytningar med den angivna texten. Som standard är texten från inmatningsdokumentet som används.

SÖK OCH ERSÄTT

Ändra dokumenttexten och strukturen med användardefinierade mönster.

--search-replace

Sökvägen till en fil som innehåller reguljära uttryck för att söka och ersätta. Filen måste innehålla alternerande rader av reguljära uttryck följt av ersättande mönster (vilket kan vara en tom rad). Det reguljära uttrycket ska vara i Python-regex-syntax och filen måste vara UTF-8-kodad.

--sr1-replace

Ersättning för att ersätta texten som hittades med SR1-sökning.

--sr1-search

Sökmönster (reguljära uttryck) att ersätta med SR1-ersättning.

--sr2-replace

Ersättning för att ersätta texten funnen med SR2-sökning.

--sr2-search

Sökmönster (reguljära uttryck) att ersätta med SR2-ersättning.

--sr3-replace

Ersättning för att ersätta texten hittades med SR3-sökning.

--sr3-search

Sökmönster (reguljära uttryck) att ersätta med SR3-ersättning.

STRUKTURDETEKTERING

Anpassa automatisk identifiering av dokumentets struktur.

--chapter

Ett XPath-uttryck för att identifiera kapitelrubrikerna. Standard är att betrakta <h1>- eller <h2>-taggar som innehåller orden "chapter", "book", "section", "prologue", "epilogue" eller "part" som kapitelrubriker samt alla taggar som har class="chapter". Uttrycket som används måste utvärderas till en lista med element. För att inaktivera kapitelidentifiering, använd uttrycket "/". Se XPath-handledningen i användarmanualen för calibre för ytterligare hjälp med att använda den här funktionen.

--chapter-mark

Anger hur upptäckta kapitel markeras. Värdet "pagebreak" infogar en sidbrytning före kapitel. Värdet "rule" infogar en tom rad före kapitel. Värdet "none" inaktiverar kapitelmarkering och om värdet "both" anges kommer både sidbrytningar och tomma rader att markera kapitel.

--disable-remove-fake-margins

Vissa dokument anger sidmarginaler genom att ange en vänster- och höger marginal på varje enskild punkt. calibre kommer att försöka identifiera och ta bort dessa marginaler. Ibland kan detta orsaka avlägsnande av marginaler som inte borde ha tagits bort. I detta fall kan du inaktivera borttagning.

--insert-metadata

Infoga bokens metadata i början av boken. Används om din läsenhet inte kan visa eller söka efter metadata direkt.

--page-breaks-before

Ett XPath-uttryck. Sidbrytningar infogas före de angivna elementen. För att inaktivera använd uttrycket: /

--prefer-metadata-cover

Använd omslaget som upptäckts från källfilen framför det angivna omslaget.

--remove-first-image

Ta bort den första bilden från den inmatade e-boken. Praktiskt om inmatningsdokumentet har en omslagsbild som inte identifieras som ett omslag. Om du anger ett omslag i calibre kommer det resulterande dokumentet ha två omslagsbilder om du inte markerar det här alternativet.

--start-reading-at

Ett XPath-uttryck för att identifiera platsen i dokumentet där du ska börja läsa. Vissa e-bokläsningsprogram (framförallt Kindle) använder denna plats som position för att öppna boken. Se XPath-handledningen i användarmanualen för calibre för ytterligare hjälp med att använda den här funktionen.

INNEHÅLLSFÖRTECKNING

Anpassa hur innehållsförteckningen skapas. Om källfilen har en innehållsförteckning, kommer denna att användas istället för den automatiskt skapade.

--duplicate-links-in-toc

När du skapar en innehållsförteckning från länkar i inmatningsdokumentet, tillåt dubbla poster, dvs. tillåt mer än en post med samma text, förutsatt att de hänvisar till en annan plats.

--level1-toc

XPath-uttryck som anger alla taggar som ska läggas till i innehållsförteckningen på nivå ett. Om detta anges har det företräde framför andra former av automatisk upptäckt. Se XPath-handledningen i användarmanualen för calibre för exempel.

--level2-toc

XPath-uttryck som anger alla taggar som ska läggas till i innehållsförteckningen på nivå två. Varje post läggs till under föregående nivå ett-post. Se XPath-handledningen i användarmanualen för calibre för exempel.

--level3-toc

XPath-uttryck som anger alla taggar som ska läggas till i innehållsförteckningen på nivå tre. Varje post läggs till under föregående nivå två-post. Se XPath-handledningen i användarmanualen för calibre för exempel.

--max-toc-links

Högst antal länkar för att infoga i innehållsförteckningen. Ställ in till 0 för att inaktivera. Standard är: 50. Länkarna läggs endast till innehållsförteckningen om antalet identifierade kapitel är lägre än tröskelvärdet.

--no-chapters-in-toc

Lägg inte till automatiskt upptäckta kapitel i innehållsförteckningen.

--toc-filter

Ta bort poster från innehållsförteckningen vilkas titlar matchar det angivna reguljära uttrycket. Matchande poster och alla deras underposter tas bort.

--toc-threshold

Om färre än detta antal kapitel upptäcks läggs länkar till i innehållsförteckningen. Standard: 6

--use-auto-toc

Om källfilen redan har en innehållsförteckning, används normalt denna i stället för den automatiskt skapade. Med det här alternativet används alltid den automatiskt skapade.

METADATA

Alternativ för att ställa in metadata i utmatning

--author-sort

Sträng att användas vid sortering av författaren.

--authors

Ange författarna. Flera författare ska avgränsas med &-tecken.

--book-producer

Ange bokens producent.

--comments

Ange e-bokbeskrivning.

--cover

Ställ in omslaget till den angivna filen eller URL:en

--isbn

Ange ISBN för boken.

--language

Ange språket.

--pubdate

Ange publiceringsdatum (antas vara i den lokala tidszonen, såvida inte tidszonen uttryckligen anges)

--publisher

Ange e-bokutgivare.

--rating

Ange betyg. Bör vara ett nummer mellan 1 och 5.

--read-metadata-from-opf, --from-opf, -m

Läs metadata från den angivna OPF-filen. Metadata som läses från den här filen åsidosätter alla metadata i källfilen.

--series

Ange serien denna e-bok tillhör.

--series-index

Ange bokens index i denna serie.

--tags

Ange taggarna för boken. Ska vara en kommaseparerad lista.

--timestamp

Ange bokens tidsstämpel (används inte längre någonstans)

--title

Ange titeln.

--title-sort

Versionen av titeln som ska användas för sortering.

FELSÖK

Alternativ som hjälper dig att felsöka konverteringen

--debug-pipeline, -d

Spara utmatning från olika stadier av konverteringsprocessen till den angivna mappen. Användbart om du är osäker på i vilket skede av konverteringsprocessen ett problem inträffar.

--verbose, -v

Nivå på informationsnivån. Ange flera gånger för ökad informationsnivå. Att ange den två gånger resulterar i full informationsnivån, en gång i medelinformationsnivån och noll gånger i minsta nivån.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- ebook-edit

ebook-edit

ebook-edit [opts] [path_to_ebook] [name_of_file_inside_book ...]

Starta calibre verktyget Redigera bok. Du kan som alternativ också ange namnen på filerna inuti boken som ska öppnas automatiskt för redigering.

När du skickar argument till ebook-edit som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--detach

Koppla från kontrollterminalen, om någon (endast Linux)

--help, -h

visa denna hjälptext och avsluta

--select-text

Texten som ska markeras i boken när den öppnas för redigering

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- ebook-meta

ebook-meta

ebook-meta ebook_file [options]

Läs/skriv metadata från/till e-bokfil.

Stödda format för läsning av metadata: 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

Stödda format för skrivning av metadata: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

Olika filtyper stöder olika sorters metadata. Om du försöker ställa in några metadata för en filtyp som inte stöder den kommer metadata att ignoreras.

När du skickar argument till ebook-meta som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--author-sort

Sträng som ska användas vid sortering av författare. Om denna inte anges och författaren eller författarna anges, kommer strängen automatiskt skapas från författarens eller författarnas namn.

--authors, -a

Ange författarna. Flera författare ska avgränsas med &-tecken. Författarnamnen bör vara i ordningen Förnamn Efternamn.

--book-producer, -k

Ange bokens producent.

--category

Ange bokens kategori.

--comments, -c

Ange e-bokbeskrivning.

--cover

Ställ in omslaget till den angivna filen.

--date, -d

Ange publiceringsdatumet.

--from-opf

Läs metadata från den angivna OPF-filen och använd dem som metadata i e-boken. Metadata som anges på kommandoraden kommer att åsidosätta metadata som lästs från OPF-filen

--get-cover

Hämta omslag från e-bok och spara det som specificerad filen.

--help, -h

visa denna hjälptext och avsluta

--identifier

Ställ in identifierare för boken, kan anges flera gånger. Till exempel: --identifier URI:https://acme.com --identifier isbn:12345 För att ta bort en identifierare, ange inte något värde, --identifier isbn: Observera att för EPUB-filer kan en identifierare markerad som paketidentifierare inte tas bort.

--index, -i

Ange bokens index i denna serie.

--isbn

Ange ISBN för boken.

--language, -l

Ange språket.

--lrf-bookid

Ange BookID i LRF-filer

--publisher, -p

Ange e-bokutgivare.

--rating, -r

Ange betyg. Bör vara ett nummer mellan 1 och 5.

--series, -s

Ange serien denna e-bok tillhör.

--tags

Ange taggarna för boken. Ska vara en kommaseparerad lista.

--title, -t

Ange titeln.

--title-sort

Titelversionen som ska användas för sortering. Om denna inte anges, och titeln är angiven, kommer den automatiskt skapas från titeln.

--to-opf

Ange namnet på en OPF-fil. Metadata kommer att skrivas till OPF-filen.

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- ebook-polish

ebook-polish

ebook-polish [options] input_file [output_file]

Polera böcker handlar om att sätta glans av perfektion på dina noggrant utformade e-böcker.

Polering försöker minimera ändringarna i den interna koden av din e-bok.
Till skillnad från konvertering, plattar den inte ut CSS, byter namn på filer, ändrar teckensnittsstorlekar, justerar marginaler osv. Varje åtgärd utför endast minsta uppsättning av ändringar som krävs för önskad effekt.

Du bör använda det här verktyget som det sista steget i din e-bokskaparprocess.

Observera att polering endast fungerar på filer av formaten AZW3 eller EPUB.

När du skickar argument till ebook-polish som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--add-soft-hyphens, -H

Lägg till mjuka bindestreck till alla ord i boken. Det här gör att boken kan renderas bättre när texten är justerad, hos läsenheter som inte stöder avstavning.

--compress-images, -i

Komprimerar förlustfritt bilder i boken, för att minska filstorleken, utan att påverka bildkvaliteten.

--cover, -c

Sökvägen till en omslagsbild. Ändrar omslag angivit i boken. Om inget omslag finns, eller omslaget inte är korrekt identifierade, infogas ett nytt omslag.

--embed-fonts, -e

Bädda in alla teckensnitt som refereras i dokumentet och som inte redan är inbäddade. Detta kommer att skanna din dator efter teckensnitt, och om de finns, kommer de att bäddas in i dokumentet. Se till att du har rätt licens för att bädda in teckensnitt som används i detta dokument.

--help, -h

visa denna hjälptext och avsluta

--jacket, -j

Infoga ett "bokomslag" i början av boken som innehåller alla om bokens metadata som titel, taggar, författare, serier, kommentarer, osv. Alla tidigare bokomslag kommer att ersättas.

--opf, -o

Sökväg till en OPF-fil. Metadata i boken uppdateras från OPF-filen.

--remove-jacket

Ta bort tidigare infogat bokomslag.

--remove-soft-hyphens

Ta bort mjuka bindestreck från all text i boken.

--remove-unused-css, -u

Ta bort alla oanvända CSS-regler från formatmallar och <format>taggar. Vissa böcker skapade från produktionsmallar kan ha ett stort antal extra CSS-regler som inte matchar något faktiskt innehåll. Dessa extra regler kan sakta ner läsenheter som behöver analysera dem alla.

--smarten-punctuation, -p

Konvertera bindestreck, ellipser, citattecken, flertal bindestreck osv. till deras typografiskt korrekta motsvarigheter. Observera att algoritmen ibland kan skapa felaktiga resultat, särskilt när enkla citattecken i början av sammandragningar är inblandade.

--subset-fonts, -f

Underuppsätta teckensnitt innebär att man reducerar ett inbäddat teckensnitt så att det endast innehåller de tecken som används från det teckensnittet i boken. Detta minskar storleken på teckensnittsfilerna (halvering av teckensnittsfilstorlekar är vanligt). Till exempel, om boken använder ett specifikt teckensnitt för rubriker, kommer underuppsättningen att minska teckensnittet så att det endast innehåller de tecken som finns i själva rubrikerna i boken. Eller om boken bäddar in de fet och kursiva versionerna av ett teckensnitt, men fet och kursiv text är relativt sällsynt, eller helt saknas, kan fetstil och kursiv teckensnitt antingen reduceras till bara några få tecken eller tas bort helt. Den enda nackdelen med att underuppsätta teckensnitt är att om du vid ett senare tillfälle bestämmer dig för att lägga till mer text i dina böcker kanske den nyligen tillagda texten inte täcks av teckensnittet för delmängden.

--upgrade-book, -U

Uppgradera bokens interna strukturer, om möjligt. Till exempel uppgraderar EPUB 2 böcker till EPUB 3 böcker.

--verbose

Producera mera utförligt utmatning, användbart för felsökning.

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- ebook-viewer

ebook-viewer

ebook-viewer [options] file

Visa en e-bok.

När du skickar argument till ebook-viewer som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--continue

Fortsätt läsa den senast öppnade boken

--detach

Koppla från kontrollterminalen, om någon (endast Linux)

--force-reload

Tvinga omladdning av alla öppnade böcker

--full-screen, --fullscreen, -f

Om valt, kommer e-bokvisarfönstret att försöka öppnas i helskärm vid start.

--help, -h

visa denna hjälptext och avsluta

--new-instance

Öppna ett nytt visningsfönster även när alternativet att använda endast ett visningsfönster är inställt

--open-at

Den position där den angivna boken ska öppnas. Positionen är en plats eller position som du kan få genom att använda åtgärden Gå till->Plats i visningskontrollerna. Alternativt kan du använda formuläret toc:något och det öppnas på platsen för den första innehållsförteckningsposten som innehåller strängen "något". Formen toc-href:något kommer att matcha href (intern länkdestination) för toc-noder. Matchningen är exakt. Om du vill matcha en delsträng använder du formen toc-href-contains:något. Formen ref:något kommer att använda referenslägesreferenser. Formuläret sök:något kommer att söka efter något efter att ha öppnat boken. Formen regex:något kommer att söka efter det reguljära uttrycket något efter att ha öppnat boken.

--raise-window

Om valt, kommer e-bokvisarfönstret att försöka lägga sig överst vid start.

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [options]

Hämta bokmetadata från nätresurser. Du måste ange minst en av titel, författare eller ISBN.

När du skickar argument till fetch-ebook-metadata som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--allowed-plugin, -p

Ange namnet på en metadatahämtningsinsticksmodul som ska användas. Som standard används alla metadatainsticksmoduler. Kan anges flera gånger för flera insticksmoduler. Alla insticksmodulnamn: Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search

--authors, -a

Bokförfattare

--cover, -c

Ange ett filnamn. Omslaget, om tillgängligt, kommer att sparas till filnamnet. Utan denna parameter kommer omslaget inte att hämtas.

--help, -h

visa denna hjälptext och avsluta

--identifier, -I

Identifierar så som ASIN/Goodreads-id osv. kan anges flera gånger för flera identifierare. Till exempel: --identifier asin:B0082BAJA0

--isbn, -i

Bok-ISBN

--opf, -o

Mata ut metadata i OPF-format istället för läsbar text.

--timeout, -d

Tidsbegränsning i sekunder. Standard är 30

--title, -t

Boktitel

--verbose, -v

Skriv loggen till konsolen (stderr)

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- lrf2lrs

lrf2lrs

lrf2lrs bok.lrf

Konvertera en LRF-fil till en LRS-fil (XML, UTF-8-kodad)

När du skickar argument till lrf2lrs som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--dont-output-resources

Spara inte inbäddade bild- och teckensnittsfiler till hårddisken

--help, -h

visa denna hjälptext och avsluta

--output, -o

Utmatning för LRS-fil

--verbose

Var mer informativ

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- lrfviewer

lrfviewer

lrfviewer [options] book.lrf

Läs LRF-e-bokens bok.lrf

När du skickar argument till lrfviewer som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--disable-hyphenation

Inaktivera avstavning. Bör snabba upp renderingen avsevärt.

--help, -h

visa denna hjälptext och avsluta

--profile

Profilera LRF-renderaren

--verbose

Skriv ut mer information om renderingsprocessen

--version

visar programmets versionsnummer och avsluta

--visual-debug

Slå på visuell hjälp för avlusning av renderingsmotorn

--white-background

Som standard är bakgrunden gulvit eftersom jag tycker att det är lättare för ögonen. Använd det här alternativet för att göra bakgrunden helvit.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- lrs2lrf

lrs2lrf

lrs2lrf [options] file.lrs

Kompilera LRF-fil från LRS-fil.

När du skickar argument till lrs2lrf som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--help, -h

visa denna hjälptext och avsluta

--lrs

Konvertera LRS till LRS, användbart för felsökning.

--output, -o

Sökväg till utmatningsfil

--verbose

Följ bearbetningsprocessen

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Kommandoradsgränssnitt »
- web2disk

web2disk

web2disk URL

Där URL är t.ex. https://google.com

När du skickar argument till web2disk som har blanksteg i dem, bifoga argumenten i citattecken. Till exempel: ”/some path/with spaces”

[alternativ]

--base-dir, -d

Basmappen där URL:en sparas. Standard är .

--delay

Minsta intervall i sekunder mellan fortlöpande hämtningar. Standard är 0 s

--dont-download-stylesheets

Hämta inte CSS-formatmallar.

--encoding

Teckenkodningen för de webbplatser du försöker hända. Standard är att försöka gissa kodningen.

--filter-regexp

Varje länk som matchar detta reguljära uttryck kommer att ignoreras. Det här alternativet kan anges flera gånger, i vilket fall så länge någon regexp matchar en länk, kommer det att ignoreras. Som standard ignoreras inga länkar. Om både filtrera regexp och matchar regexp specificeras, då används filtrera regexp först.

--help, -h

visa denna hjälptext och avsluta

--match-regexp

Endast länkar som matchar det reguljära uttrycket kommer att följas. Det här alternativet kan anges flera gånger, då kommer länkar att följas så länge de matchar det reguljära uttrycket. Som standard följs alla länkar.

--max-files, -n

Högsta antalet filer att hämta. Endast för filer med <a href> taggar. Standard är 9223372036854775807

--max-recursions, -r

Högst antal nivåer som ska återkomma, dvs. djupet på länkar som ska följas. Standard 1

--timeout, -t

Tidsgräns i sekunder att vänta på ett svar från servern. Standard: 10.0

--verbose

Visa detaljerad utmatningsinformation. Användbart för felsökning

--version

visar programmets versionsnummer och avsluta

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Inrätta en calibre-utvecklingsmiljö

Inrätta en calibre-utvecklingsmiljö
calibre har helt öppen källkod, licensierad under GNU GPL v3 [https://www.gnu.org/licenses/gpl.html]. Detta innebär att du är fri att hämtar och ändra programmet hur mycket du vill. I det här avsnittet får du lära dig hur du inrättar en calibre-utvecklingsmiljö på valfritt operativsystem. calibre är främst skriven i Python [https://www.python.org] med lite C/C++-kod för hastighet och systemgränssnitt. Observera att calibre kräver minst Python 3.8.

Innehåll

- Konstruktionsfilosofi

- Kodutformning

- Få koden

- Skicka in dina ändringar som ska ingå

- Windows-utvecklingsmiljö

- macOS-utvecklingsmiljö

- Linux-utvecklingsmiljö

- Att ha separata ”normala”- och ”utvecklings”-installationer av calibre på samma dator

- Felsökningstips

- Använda utskriftssatser

- Använda en interaktiv Python-tolk

- Använda Python-felsökaren som fjärrfelsökare

- Använda felsökaren i din favorit Python-IDE

- Köra godtyckliga skript i Python-miljön för calibre

- Använda calibre i dina projekt

- Binär installation av calibre

- Källkodsinstallation på Linux

- API-dokumentation för olika delar av calibre

Konstruktionsfilosofi
calibre har sina rötter i Unix-världen, vilket innebär att dess design är mycket modulär. Modulerna samverkar med varandra via väl definierade gränssnitt. Detta gör det mycket lätt att lägga till nya funktioner och rätta problem i calibre, vilket resulterar i en frenetisk utvecklingstakt. På grund av sina rötter har calibre ett omfattande kommandoradsgränssnitt för alla dess funktioner, dokumenterat i Kommandoradsgränssnitt.

Den modulära uppbyggnaden av calibre uttrycks via Insticksmoduler. Det finns en handledning om att skriva calibre-insticksmoduler. Till exempel för att lägga till stöd för en ny enhet i calibre behövs vanligtvis att man skriver mindre än 100 rader kod i form av en enhetsdrivrutininsticksmodul. Du kan bläddra bland de inbyggda drivrutinerna [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices]. På samma sätt, för att lägga till stöd för nya konverteringsformat innebär det att skriva inmatnings-/utmatningsformatsinsticksmoduler. Ett annat exempel på den modulära designen är receptsystemet för att hämta nyheter. För fler exempel på insticksmoduler som är utformade för att lägga till funktioner i calibre, se Index of plugins [https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767].

Kodutformning

All calibre-Pythonkod finns i paketet calibre. Paketet innehåller följande huvudunderpaket

- enheter - Alla enhetsdrivrutiner. Titta bara igenom några av de inbyggda drivrutinerna för att få en uppfattning om hur de fungerar.

- För mer information, se: devices.interface som definierar gränssnitt stöds av drivrutiner och devices.usbms som definierar en generisk drivrutin som ansluter till en USBMS enhet. Alla USBMS baserade drivrutiner i calibre ärver från den.

- e-böcker - All e-bokkonvertering/metadatakod. En bra utgångspunkt är calibre.ebooks.conversion.cli vilken är modulen som driver ebook-convert kommandot. Konverteringen styrs via conversion.plumber. Formatoberoende kod är alla i ebooks.oeb och formatberoende kod finns i ebooks.format_name.

- Metadataläsning, skrivning, och hämtning finns i ebooks.metadata

- Konvertering sker i en process, för strukturen av processen, se Introduktion. Processen består av en inmatningsinsticksmodul, olika omvandlingar och en utmatningsinsticksmodul. Koden som konstruerar och driver processen i plumber.py. Processen fungerar på en representation av en e-bok som är som en uppackad EPUB, med manifest, bokrygg, innehållsförteckning, guide, html-innehåll, o.s.v. Klassen som hanterar den här representationen är OEBBook i ebooks.oeb.base . De olika omvandlingarna som tillämpas på boken under konvertering finns i oeb/transforms/*.py. Och in- och utmatningsinsticksmodulerna finns i conversion/plugins/*.py.

- E-bokredigering sker med hjälp av en annat kontainerobjekt. Det är dokumenterat i API-dokumentation för e-bokredigeringsverktygen.

- db - Databasens backend. Se API-dokumentation för databasgränssnittet för gränssnittet till calibre-biblioteket.

- Innehållsserver: srv är calibre-innehållsservern.

- gui2 - Grafiska användargränssnittet. Grafiska gränssnittet initialiseringen händer i gui2.main``och ``gui2.ui. E-bokvisaren finns i gui2.viewer. E-bokredigeraren finns i gui2.tweak_book.

Om du vill hitta startplatser för alla olika calibre körbara delar, titta på entry_points-strukturen i linux.py [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py].

Om du behöver hjälp med att förstå koden, skicka inlägg i det så kallade utvecklingsforumet <https://www.mobileread.com/forums/forumdisplay.php?f=240>`_ och du kommer sannolikt att få hjälp från en av calibres många utvecklare .

Få koden

Du kan hämta calibre-källkoden på två sätt, med hjälp av ett versionshanteringssystem eller direkt hämta en tarball [https://calibre-ebook.com/dist/src].

calibre använder Git [https://www.git-scm.com/], ett distribuerat versionshanteringssystem. Git är tillgänglig på alla plattformar som calibre stöder. När du har installerat Git kan du få calibre-källkoden med kommandot:

git clone https://github.com/kovidgoyal/calibre.git

På Windows behöver du det fullständiga sökvägsnamnet som kommer att vara något i stil med C:\Program Files\Git\git.exe.

calibre är ett mycket stort projekt med en mycket lång källkontrollshistorik, så ovanstående kan ta ett tag (10 minuter till en timme beroende på din internet hastighet).

Om du vill hämta koden snabbare är källkoden för den senaste utgåvan alltid tillgänglig som ett arkiv [https://calibre-ebook.com/dist/src].

För att uppdatera en filial till den senaste koden använder du kommandot:

git pull --no-edit

Du kan också se koden på GitHub [https://github.com/kovidgoyal/calibre].

Skicka in dina ändringar som ska ingå

Om du bara planerar att göra några små ändringar kan du göra dina ändringar och skapa ett ”sammanslagningsdirektiv” som du sedan kan fästa till ett ärende i calibre-felhanteringssystemet [https://bugs.launchpad.net/calibre]. För att göra detta, gör dina ändringar, kör sedan:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

Detta kommer att skapa en file:my-changes-fil i den aktuella mappen, helt enkelt bifoga det till ett ärende i calibre-felhanteringssystemet [https://bugs.launchpad.net/calibre]. Observera att detta kommer att omfatta alla de åtgärder du har gjort. Om du bara vill skicka några åtgärder måste du ändra origin/master ovan. Om du vill skicka bara den senaste bekräftelsen, använd:

git format-patch HEAD~1 --stdout > my-changes

Om du vill skicka de senaste n ändringar, ersätt 1 med n , till exempel under de senaste 3 ändringarna:

git format-patch HEAD~3 --stdout > my-changes

Var noga med att inte inkludera sammanslagningar när du använder HEAD~n.

Om du planerar att göra en hel del utveckling på calibre, är den bästa metoden att skapa ett GitHub [https://github.com]-konto. Nedan följer en grundläggande guide för att ställa in din egen förgrening av calibre på ett sätt som gör att du kan skicka förfrågningar för inkludering i calibre-huvudförrådet:

- Installering av git på din maskin som beskrivs i denna artikel: Setup Git [https://help.github.com/articles/set-up-git]

- Ställ in ssh-nycklar för autentisering till GitHub, som beskrivs här: Generating SSH keys [https://help.github.com/articles/generating-ssh-keys]

- Gå till https://github.com/kovidgoyal/calibre och klicka på knappen Fork.

- I en terminal utför:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

Ersätt <username> ovan med ditt användarnamn på Github. Det kommer att få din förgrening kontrollerad lokalt.

- Du kan göra ändringar och skicka in dem när du vill. När du är redo att slå samman ditt arbete, gör en:

git push

och gå till https://github.com/<username>/calibre och klicka på knappen Pull Request för att skapa en hämtningsbegäran som kan slås samman.

- Du kan när som helst uppdatera din lokala kopia med kod från huvudarkivet genom att göra:

git pull upstream

Du bör också hålla ett öga på calibre-utvecklingsforumet [https://www.mobileread.com/forums/forumdisplay.php?f=240]. Innan du gör stora ändringar bör du diskutera dem i forumet eller kontakta Kovid direkt (hans e-postadress är över hela källkoden).

Windows-utvecklingsmiljö

Observera

Du måste också hämta calibre-källkoden separat enligt beskrivningen ovan.

Installera calibre normalt med hjälp av installationsprogrammet för Windows [https://calibre-ebook.com/download_windows]. Öppna sedan en kommandotolk och ändra till den tidigare kontrollerade calibre-kodmappen. Till exempel:

cd C:\Users\kovid\work\calibre

calibre är mappen som innehåller src och undermappar för resurser.

Nästa steg är att ställa in miljövariabeln CALIBRE_DEVELOP_FROM till den absoluta sökvägen för mappen src. Så efter exemplet ovan skulle det vara C:\Users\kovid\work\calibre\src. Här är en kort guide [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] för att ställa in miljövariabler på Windows.

När du har ställt in miljövariabeln, öppna en ny kommandotolk och kontrollera att den blev korrekt inställd genom att använda kommandot:

echo %CALIBRE_DEVELOP_FROM%

Ställa denna miljövariabel innebär att calibre kommer nu läsa in all sin Python-kod från den angivna platsen.

Det är allt! Du är nu redo att börja hacka calibre-koden. Till exempel öppna filen src\calibre\__init__.py i din favorit redigerare och lägg till raden:

print("Hello, world!")

nära toppen av filen. Kör nu kommandot calibredb . Den allra första raden av utmatningen bör vara Hej världen!.

Du kan också ställa in en calibre-utvecklingsmiljö i gratis Microsoft Visual Studio, om du vill, genom att följa instruktionerna här [https://www.mobileread.com/forums/showthread.php?t=251201].

macOS-utvecklingsmiljö

Observera

Du måste också hämta calibre-källkoden separat enligt beskrivningen ovan.

Installera calibre normalt med hjälp av den tillhandahållna .dmg [https://calibre-ebook.com/download_osx]. Öppna sedan en terminal och ändra till den tidigare kontrollerade calibre-kodmappen, till exempel:

cd /Users/kovid/work/calibre

calibre är mappen som innehåller src och undermapparna för resurser. Kommandoradsverktygen för calibre finns i calibre-appaketet, i /Applications/calibre.app/Contents/MacOS bör du lägga till den här mappen till din PATH-miljövariabel, om du enkelt vill kunna köra kommandoradsverktygen.

Nästa steg är att skapa ett bash-skript som kommer att ställa in miljövariabeln CALIBRE_DEVELOP_FROM till den absoluta sökvägen till mappen src när calibre körs i felsökningsläge.

Skapa en fil med oformaterad text:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

Spara den här filen som /usr/local/bin/calibre-develop och ställ sedan in dess behörigheter så att den kan köras:

chmod +x /usr/local/bin/calibre-develop

När du har gjort detta, kör:

calibre-develop

Du bör se en del diagnostisk information i terminalfönstret när calibre startar, och du bör se en asterisk efter versionsnumret i grafiska gränssnittsfönstret, vilket indikerar att du kör från källkod.

Linux-utvecklingsmiljö

Observera

Du måste också hämta calibre-källkoden separat enligt beskrivningen ovan.

calibre är främst utvecklat på Linux. Du har två val i inrättandet av utvecklingsmiljön. Du kan installera calibre-binär som vanligt och använda det som en driftstidsmiljö för att göra din utveckling. Detta tillvägagångssätt liknar den som används i Windows och macOS. Alternativt kan du installera calibre från källan. Instruktioner för hur du installerar en utvecklingsmiljö från källan är i installationsfilen i källkodsträdet. Här kommer vi att ta itu med den binär som en körbar, vilket är den rekommenderade metoden.

Installera calibre med hjälp av det binära installationsprogrammet [https://calibre-ebook.com/download_linux]. Öppna sedan en terminal och ändra till den tidigare kontrollerade calibre-kodmappen, till exempel:

cd /home/kovid/work/calibre

calibre är mappen som innehåller src och undermappar för resurser.

Nästa steg är att ställa in miljövariabeln CALIBRE_DEVELOP_FROM till den absoluta sökvägen till mappen src. Så, efter exemplet ovan, skulle det vara /home/kovid/work/calibre/src. Hur man ställer in miljövariabler beror på din Linux-distribution och vilket skal du använder.

Observera

Det rekommenderas att använda det binära installationsprogrammet som tillhandahålls uppströms. Om du insisterar på att använda ett paket som tillhandahålls av din distribution, använd variablerna CALIBRE_PYTHON_PATH och CALIBRE_RESOURCES_PATH istället.

När du har ställt in miljövariabeln, öppna en ny terminal och kontrollera att den blev korrekt inställd genom att använda kommandot:

echo $CALIBRE_DEVELOP_FROM

Ställa denna miljövariabel innebär att calibre kommer nu läsa in all sin Python-kod från den angivna platsen.

Det är allt! Du är nu redo att börja hacka calibre-koden. Till exempel öppna filen src/calibre/__init__.py i din favorit redigerare och lägg till raden:

print("Hello, world!")

nära toppen av filen. Kör nu kommandot calibredb . Den allra första raden av utmatningen bör vara Hej världen!.

Att ha separata ”normala”- och ”utvecklings”-installationer av calibre på samma dator
calibre-källkodsträdet är mycket stabilt och går sällan sönder, men om du känner behov av att köra från källan i ett separat testbibliotek och köra den släppta calibre-versionen med ditt vardagliga bibliotek kan du enkelt uppnå detta med hjälp av .bat-filer eller skalskript för att starta calibre. Exemplet nedan visar hur man gör detta på Windows med .bat-filer (instruktionerna för andra plattformar är desamma, använd bara ett skalskript istället för en .bat-fil)

För att starta den utgivna versionen av calibre med ditt vardagliga bibliotek:

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"

Felsökningstips

Python är ett dynamiskt typat språk med utmärkta faciliteter för introspektion. Kovid skrev kärndelar av calibrekod utan en gång med hjälp av ett avlusningsprogram. Det finns många strategier för att felsöka calibre-kod:

Använda utskriftssatser

Det här är Kovids favoritsätt att felsöka. Infoga helt enkelt utskriftssatser vid intressanta platser och kör ditt program i terminalen. Till exempel kan du starta det grafiska gränssnittet från terminalen med:

calibre-debug -g

På samma sätt kan du starta e-bokvisaren med:

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

Använda en interaktiv Python-tolk

Du kan infoga följande två rader kod för att starta en interaktiv Python-session på den punkten:

from calibre import ipython
ipython(locals())

När du kör från kommandoraden startar detta en interaktiv Python-tolk med tillgång till alla lokalt definierade variabler (variabler i den lokala omfånget). Den interaktiva prompten har till och med Tab-komplettering för objektegenskaper och du kan använda de olika Python-faciliteterna för introspektion, såsom dir(), type(), repr() o.s.v.

Använda Python-felsökaren som fjärrfelsökare

Du kan använda den inbyggda Python-felsökaren (pdb) som en fjärrfelsökare från kommandoraden. Starta först fjärrfelsökaren vid den punkt i calibre-koden du är intresserad av, så här:

from calibre.rpdb import set_trace
set_trace()

Kör sedan calibre, antingen som normalt, eller använda någon av calibre-felsökningskommandon som beskrivs i föregående avsnitt. När ovanstående punkt i koden är nådd kommer calibre frysa, väntar på felsökaren för att ansluta.

Öppna nu en terminal eller kommandotolk och använd följande kommando för att starta felsökningssessionen:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Du kan läsa om hur man använder Python-felsökaren i Python stdlib docs för pdb-modulen [https://docs.python.org/library/pdb.html#debugger-commands].

Observera

Som standard kommer fjärrfelsökaren att försöka ansluta på port 4444. Du kan ändra det genom att skicka portparametern till både set_trace()- och cli()-funktionerna ovan, så här: set_trace(port=1234) och cli(port=1234).

Observera

Python-felsökaren kan inte hantera flera trådar, så du måste anropa set_trace en gång per tråd, varje gång med ett annat portnummer.

Använda felsökaren i din favorit Python-IDE

Det är möjligt att använda den inbyggda felsökaren i din favorit Python-IDE, om den stöder fjärrfelsökning. Det första steget är att lägga till utcheckat calibre src till PYTHONPATH i IDE. Med andra ord, mappen du anger som CALIBRE_DEVELOP_FROM måste ovan, också vara i PYTHONPATH av din IDE.

Placera sedan IDE-fjärrfelsökarmodulen i undermappen :file:`src`för calibre-källkoden. Lägg allt vilken kod som behövs för att starta fjärrfelsökaren i calibre vid intressepunkten, till exempel i huvudfunktionen. Kör sedan calibre som vanligt. Din IDE bör nu kunna ansluta till fjärrfelsökaren som körs inuti calibre.

Köra godtyckliga skript i Python-miljön för calibre

Kommandot :command:`calibre-debug`ger ett par praktiska flaggor för att köra din egen kod, med tillgång till calibre-modulerna:

calibre-debug -c "some Python code"

är bra för att testa ett litet kodavsnitt på kommandoraden. Det fungerar på samma sätt som den -c flaggan till Python-tolken:

calibre-debug myscript.py

kan användas för att köra ditt eget Python-skript. Det fungerar på samma sätt som att skicka skriptet till Python-tolken, förutom att calibre-miljön är helt initierad, så att du kan använda all calibre-kod i skriptet. För att använda kommandoradsargument med ditt skript, använd formuläret:

calibre-debug myscript.py -- --option1 arg1

-- gör att alla efterföljande argument skickas till ditt skript.

Använda calibre i dina projekt

Det är möjligt att direkt använda calibre-funktioner/-kod i ditt Python-projekt. Det finns två sätt att göra detta:

Binär installation av calibre

Om du har en binär installation av calibre, kan du använda Python-tolken som medföljer calibre, så här:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Källkodsinstallation på Linux

Förutom att använda ovanstående teknik, om du gör en källkodsinstallation på Linux, kan du också direkt importera calibre, enligt följande:

import init_calibre
import calibre

print(calibre.__version__)

Det är viktigt att du importerar modulen init_calibre före andra calibre-moduler/-paket eftersom den ställer in tolken för att köra calibre-kod.

API-dokumentation för olika delar av calibre

- API-dokumentation för recept
- API-dokumentation för insticksmoduler
- API-dokumentation för databasgränssnittet

- API-dokumentation för e-bokredigeringsverktygen

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Inrätta en calibre-utvecklingsmiljö »
- API-dokumentation för databasgränssnittet

API-dokumentation för databasgränssnittet

Denna API är trådsäker (använder ett flerläsar-, singelskrivarutelåsningsschema). Du kan komma åt denna API så här:

from calibre.library import db
db = db('Path to calibre library folder').new_api

Om du är i en calibre-insticksmodul som är en del av calibres huvudanvändargränssnitt får du tillgång till den så här istället:

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[source]

En cache i minnet för metadata.db-filen från ett calibre-bibliotek. Den här klassen fungerar också som ett trådsäkert API för åtkomst till databasen. Cachen i minnet behålls i normal form för högsta prestanda.

SQLITE används helt enkelt som ett sätt att läsa och skriva från metadata.db robust. All logik för läsning/sortering/sökning/cachning av tabeller är återimplementerad. Detta var nödvändigt för maximal prestanda och flexibilitet.

class EventType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

book_created = 4

När en ny bokpost skapas i databasen, med bokidentitet som enda argument

book_edited = 8

När ett bokformat redigeras, med argument: (book_id, fmt)

books_removed = 5

När böcker tas bort från databasen med listan över bok-ID som enda argument

format_added = 2

När ett format läggs till i en bok, med argument: (bok_id, format)

formats_removed = 3

När format tas bort från en bok, med argument: (mappning av bok-id till uppsättning format borttagna från boken)

indexing_progress_changed = 9

När indexeringsförloppet ändras

items_removed = 7

När objekt som taggar eller författare tas bort från vissa böcker. Argument: (field_name, berörda bok-id, id för borttagna objekt)

items_renamed = 6

När objekt som taggar eller författare byter namn i vissa eller alla böcker. Argument: (fältnamn, påverkade bok-id:er, mappning av gammalt post-id till nytt post-id)

metadata_changed = 1

När vissa metadata ändras för vissa böcker, med argument: (namn på ändrat fält, uppsättning påverkade bok-id)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[source]

Lägg till de angivna böckerna i biblioteket. Böcker bör vara en uppräknelig mängd i multipel av 2, varje multipel av 2 med formen (mi, format_map) där mi är ett metadataobjekt och format_map är en ordbok med formen {fmt: path_or_stream}, till exempel: {'EPUB': '/sökväg/till/fil.epub'}.

returnerar ett listpar: ids, duplicates. id innehållande bokens identiteter för all nyligen skapade böcker i databasen. ”duplikat” innehåller (mi, format_map) for alla böcker som redan finns i databasen genom enkel heuristisk duplikationsdetektering använd av has_book().

add_custom_book_data(name, val_map, delete_first=False)[source]

Lägg till data för namn där val_map är en karta över book_ids till värden. Om delete_first är True kommer alla tidigare lagrade data för namn tas bort.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[source]

Lägg till extra datafiler

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[source]

Lägg till ett format i den angivna boken. Returnera True om formatet har lagts till.

Parametrar:

- replace – Om True ersätts befintligt format, annars om formatet redan finns, returneras False.

- run_hooks – Om True, körs filtypsinsticksmoduler på formatet innan och efter de tilläggs.

- dbapi – Endast internt bruk.

add_listener(event_callback_function, check_already_added=False)[source]

Registrera en återanropningsfunktion som anropas efter att vissa åtgärder har vidtagits i denna databas. Funktionen måste ha tre argument: (EventType, library_id, event_type_specific_data)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[source]

Lägg till den angivna resursen så att den kan refereras av anteckningar och returnera dess innehållshash

all_book_ids(type=<class 'frozenset'>)[source]

Fryst uppsättning av alla kända bokidentiteter.

all_field_for(field, book_ids, default_value=None)[source]

Samma som field_for, förutom att det fungerar på flera böcker samtidigt

all_field_ids(name)[source]

Fryst uppsättning av identiteter för alla värden i fältet namn.

all_field_names(field)[source]

Fryst uppsättning av alla fältnamn (bör endast användas för många-ett och många-många fält)

author_data(author_ids=None)[source]

returnerar författardata som ordbok med nycklar: namn, sort, länk

Om inga författare med angivna identiteter hittas returneras en tom ordbok. Om author_ids är None, returneras data för alla författare.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[source]

Given en lista med författare, returnera author_sort sträng för författare, framför författarsortering associerad med författare över bearbetade strängen.

books_for_field(name, item_id)[source]

returnerar alla böcker associerade med posten identifierad av item_id, där posten tillhör fältet name.

Returnerat värde är en mängd av bokidentiteter eller den tomma mängden om posten eller fältet inte finns.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[source]

Returnera uppsättningen böcker i det angivna virtuella biblioteket

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[source]

Komprimera omslagsbilderna för de angivna böckerna. En komprimeringskvalitet på 100 kommer att utföra förlustfri komprimering, annars är det förlustbringande.

Framstegsåterkallningen kommer att anropas med book_id och de gamla och nya storlekarna för varje bok som har bearbetats. Om ett fel inträffar kommer den nya storleken att vara en sträng med feldetaljer.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[source]

Kopiera omslag till filen som objektet dest. Returnerar False om inget omslag finns eller dest är samma fil som det aktuella omslaget. dest kan också vara en sökväg i vilket fall omslaget kopieras till det om och endast om sökvägen är en annan än den aktuella sökvägen (med små bokstäver i beräkningen).

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[source]

Kopiera formatet fmt till det fillika objektet dest. Om det angivna formatet inte finns, ges NoSuchFormat-fel. dest kan också vara en sökväg (till en fil), i vilket fall formatet kopieras till den, om och endast om sökvägen skiljer sig från den aktuella sökvägen (med hänsyn till skiftlägeskänslighet).

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[source]

Returnera omslagsbild eller inget. Som standard, returneras omslag som en byte sträng.

VARNING: Använda as_path kommer att kopiera omslaget till en temporär fil och returnera sökvägen till temporära filen. Du bör ta bort temporära filen när du är klar med den.

Parametrar:

- as_file – Om True returnerar bilden som ett öppet filobjekt (en SpooledTemporaryFile)

- as_image – Om True returnerar bilden som ett Q-bildobjekt

- as_pixmap – Om True returnera bilden som ett QPixmap-objekt

- as_path – Om True returnerar bilden som en sökväg till en temporärfil

data_for_find_identical_books()[source]

Returnera data som kan användas för att implementera find_identical_books() i en arbetsprocess utan tillgång till db. Se db.utils för en implementering.

data_for_has_book()[source]

returnerar data som är lämpliga för användning i has_book(). Detta kan användas för en implementering av has_book() i en arbetsprocess utan tillgång till db.

delete_custom_book_data(name, book_ids=())[source]

Ta bort data för namn. Som standard tas alla data bort, om du endast vill ta bort data för vissa bokidentifierare, passera i en lista med bokidentifierare.

delete_trash_entry(book_id, category)[source]

Ta bort en post från papperskorgen. Här är kategorin ’b’ för böcker och ’f’ för format.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[source]

Uppdatera metadata i alla format av de angivna bok_ids till aktuell metadata i databasen.

expire_old_trash()[source]

Förfalla poster från papperskorgen som är för gamla

export_note(field, item_id) → str[source]

Exportera anteckningen som ett enda HTML-dokument med inbäddade bilder som data: URL:er

fast_field_for(field_obj, book_id, default_value=None)[source]

Samma som field_for, förutom att den undviker den extra uppslagningen för att få fältobjektet

field_for(name, book_id, default_value=None)[source]

Returnera värdet av fältet name för boken som identifieras av book_id. Om ingen sådan bok finns eller inte har något definierat värde för fältet name eller inget sådant fält finns, så returneras default_value.

default_value används inte för title, title_sort, authors, author_sort och series_index. Det här beror på att dessa alltid har värden i db. default_value används för alla anpassade kolumner.

Det återgivna värdet för is_multiple fält är alltid tupler, även om inga värden finns (med andra ord, default_value ignoreras). Undantaget är identifierare som det återgav värdet alltid är en ordbok. De återgivna tuplerna är alltid i länkordning, det vill säga i vilken ordning de skapades.

field_ids_for(name, book_id)[source]

Returnera id:et (som en tupel) för de värden som fältet namn har på boken som identifieras av book_id. Om det inte finns några värden, ingen sådan bok, eller inget sådant fält, returneras en tom tupel.

field_supports_notes(field=None) → bool[source]

Returnera True om det angivna fältet stöder anteckningar. Om fältet är Inget returnera frozenset av alla fält som stöder anteckningar.

find_identical_books(mi, search_restriction='', book_ids=None)[source]

Hittar böcker som har en övermängd av författarna i mi och samma titel (titeln är oklart matchad). Se även data_for_find_identical_books().

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[source]

Returnera e-bokformat som en bytesträng eller None om formatet inte finns, eller om vi inte har behörighet att skriva till e-bokfilen.

Parametrar:

- as_file – Om True returneras e-bokformatet som ett filobjekt. Observera att filobjektet är en SpooledTemporaryFile, så om vad du vill göra är att kopiera formatet till en annan fil, använd copy_format_to() istället för prestanda.

- as_path – Kopierar formatet filen till en temp fil och returnerar sökvägen till temp filen

- preserve_filename – Om True och returneras en sökväg, filnamnet är samma som den som används i biblioteket. Observera att detta innebär att upprepade anrop ger samma temp fil (som återskapas varje gång)

format_abspath(book_id, fmt)[source]

Returnera absolut sökväg till e-bokfilen i formatet format. Du bör nästan aldrig använda det, eftersom det bryter trådsäkerhetsgarantin i detta API. Använd istället, copy_format_to().

Används för närvarande endast i calibredb-listan, visaren, redigera bok, compare_format till originalformat, öppna med, redigera metadata i grupp och katalogerna (via get_data_as_dict()).

Bortsett från visaren, öppna med och redigera boken, tror jag inte någon av de andra gör någon filskrivnings-I/O med resultaten av detta anrop.

format_hash(book_id, fmt)[source]

Returnera hash för det angivna formatet för den angivna boken. Den typ av hash är algoritmberoende, men är oftast SHA-256.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[source]

Returnera sökväg, storlek och mtime för det specifika formatet för den specifika boken. Du bör inte använda sökväg annat om du absolut måste, eftersom direkt tillgång av den förstör trådsäkerhetsgarantin av denna API. Istället använd copy_format_to() metoden.

Parametrar:

- allow_cache – Om True, används cachade värden, annars sker en långsam åtkomst till filsystemet. Cachevärdena kan vara inaktuella om åtkomst sker till filsystem utanför detta API.

- update_db – Om True kommer max_size-fältet i databasen uppdateras för denna bok.

formats(book_id, verify_formats=True)[source]

Returnera tupel av alla format för den specifika boken. Om verify_formats är True verifieras att filer finns på disken.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][source]

Returnera alla item_ids för poster som har anteckningar i det angivna fältet eller alla fält om fältnamn är None

get_all_link_maps_for_book(book_id)[source]

Returnerar alla länkar för alla fält som hänvisas till av bok som identifieras av book_id. Om book_id inte finns returnerar metoden {}.

Exempel: Antag att författare A har länk X, författare B har länk Y, tagg S har länk F och tagg T har länk G. Om bok 1 har författare A och tagg T, returnerar denna metod {’authors’:{’A’:’X’}, ’tags’:{’T’, ’G’}}. Om bok 2:s författare varken är A eller B och inte har några taggar, returnerar denna metod {}.

Parametrar:

book_id – bokidentitet i fråga.

Returnerar:

{field: {field_value, link_value}, … för alla fält med ett field_value som har ett icke-tomt länkvärde för den boken

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[source]

Används internt för att implementera taggbläddraren

get_custom_book_data(name, book_ids=(), default=None)[source]

Hämta data för namn. Som standard returneras data för alla book_ids, förse med en lista med bokidentifierare om du bara vill ha några data. returnerar en översikt av book_id till värden. Om en speciellt värde inte kan identifieras, används istället standardvärde för den.

get_id_map(field)[source]

Returnera en mappning av id-nummer till värden för det angivna fältet. Fältet måste vara ett många-en- eller många-många-fält, annars uppstår ett ValueError.

get_ids_for_custom_book_data(name)[source]

Returnera en uppsättning bok-id för vilket namn har data.

get_item_id(field, item_name)[source]

Returnera post-id för item_name (skiftlägeskänsligt) eller None om den inte hittas

get_item_ids(field, item_names)[source]

Returnera posten id för item_name (skiftlägesokänslig)

get_item_name(field, item_id)[source]

Returnera postnamnet för posten som anges av item_id i specificerat fält. Se också 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]

Returnera en ordbok med länkar för det angivna fältet.

Parametrar:

for_field – uppslagsnamnet för fältet för vilket länkkartan önskas

Returnerar:

{field_value:link_value, …} för icke-tomma länkar

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[source]

Returnera metadata för boken identifierat av book_id som ett calibre.ebooks.metadata.book.base.Metadata objekt. Observera att listan av format inte är verifierad. Om get_cover är True, kommer omslag att returneras, antingen som en sökväg till temporärfilen mi.cover eller om cover_as_data är Sant som mi.cover_data.

get_next_series_num_for(series, field='series', current_indices=False)[source]

Returnera nästa serieindex för den specifika serien, med beaktning av olika inställningar som styr skapande av nästa serienummer.

Parametrar:

- field – Det serieliknande fältet (standard är den inbyggda seriekolumnen)

- current_indices – Om True, returneras en mappning av book_id till aktuellt serie_index-värde istället.

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]

Liksom: meth:get_metadata förutom att den returnerar ett ProxyMetadata-objekt som bara läser värden från databasen på förfrågan. Det är mycket snabbare än get_metadata när endast ett mindre antal fält behöver nås från returnerat metadataobjekt.

get_usage_count_by_id(field)[source]

Returnera en mappning av id till användningsantal för alla värden i det angivna fältet, som måste vara ett många-en- eller många-många-fält.

has_book(mi)[source]

Returnera True om och endast om databasen innehåller en post med samma titel som den angivna i metadataobjektet. Jämförelsen är skiftlägeskänslig. Se även data_for_has_book().

has_format(book_id, fmt)[source]

Returnera True om och endast om formatet finns på disken

has_id(book_id)[source]

Returnera True om och endast om specificerad bok_id finns i db

import_note(field, item_id, path_to_html_file, path_is_data=False)[source]

Importera en tidigare exporterad anteckning eller en godtycklig HTML-fil som anteckning för den angivna posten

init()[source]

Initiera denna cache med data från backend.

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]

Returnera länken, om någon, för det angivna objektet eller None om ingen länk hittas

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][source]

Få information om extra filer i bokens mapp.

Parametrar:

- book_id – databasens bokidentitet för boken

- pattern – mönstret av filnamn att söka efter. Tomt mönster matchar alla extra filer. Mönster måste använda / som separator. Använd konstanten DATA_FILE_PATTERN för att matcha filer i datamappen.

Returnerar:

En tupel av alla extra filer som matchar det angivna mönstret. Varje element i tuplen är ExtraFile(relpath, file_path, stat_result). Där relpath är den relativa sökvägen för filen till bokkatalogen med / som avgränsare. stat_result är resultatet av att anropa os.stat() på filen.

merge_extra_files(dest_id, src_ids, replace=False)[source]

Slå samman de extra filerna från src_ids till dest_id. Motstridiga filer döps automatiskt om inte replace=True i vilket fall de ersätts.

move_book_from_trash(book_id)[source]

Ångra borttagning av en bok från papperskorgen

move_format_from_trash(book_id, fmt)[source]

Ångra borttagning av ett format från papperskorgen

multisort(fields, ids_to_sort=None, virtual_fields=None)[source]

Returnera en lista med sorterade bokidentifierare. Om ids_to_sort är None, kommer alla bokidentifierare att returneras.

fälten måste vara en lista med 2-tupler av formatet (field_name, ascending=True eller False). Det signifikantaste fältet är den första 2-tupeln.

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]

Returnera anteckningsdokumentet eller en tom sträng om den inte hittas

notes_resources_used_by(field, item_id)[source]

Returnera uppsättningen resurs-hashar för alla resurser som används av anteckningen för det angivna posten

pref(name, default=None, namespace=None)[source]

Returnera värdet för det specifika förvalet eller värdet angivet som standard om ingen inställning är angivet.

read_backup(book_id)[source]

Returnera OPF-metadata-säkerhetskopian för boken som en bytesträng eller None om ingen sådan säkerhetskopia finns.

remove_books(book_ids, permanent=False)[source]

Ta bort böckerna som specificeras av book_ids från databasen och ta bort deras formatfiler. Om permanent är False, placeras formatfilerna i papperskorgen per bibliotek.

remove_formats(formats_map, db_only=False)[source]

Ta bort de angivna formaten från de angivna böckerna.

Parametrar:

- formats_map – En mappning av book_id till en lista med format som ska tas bort från boken.

- db_only – Om True, tas bara handlingen av format från databasen, tar inte bort de aktuella filformaten från filsystemet.

Returnerar:

En karta över bok-id till uppsättning format som faktiskt tagits bort från filsystemet för den boken

remove_items(field, item_ids, restrict_to_book_ids=None)[source]

Ta bort alla poster i det angivna fältet med det angivna id. returnerar uppsättningen av påverkade bok-id. restrict_to_book_ids är en valfri uppsättning bok-id. Om det anges kommer posterna endast att tas bort från dessa böcker.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[source]

Byt namn på extra datafiler

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[source]

Byt namn på post från många-en eller många-många fält som taggar eller serier.

Parametrar:

- change_index – Vid byte av namn i ett serieliknande fält ändras också värdena för series_index.

- restrict_to_book_ids – En valfri uppsättning bok-id där namnbyte ska utföras, standard för alla böcker.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[source]

Återställ bokposten i databasen för en bok som redan finns i filsystemet

restore_original_format(book_id, original_fmt)[source]

Återställer det angivna formatet från tidigare sparat ORIGINAL_FORMAT, om något. returnerar True vid framgång. The ORIGINAL_FORMAT tas bort efter en lyckad återställning.

property safe_read_lock

Ett säkert läslås är ett lås som inte gör något om tråden redan har ett skrivlås, annars får den ett läslås. Detta är nödvändigt för att förhindra DowngradeLockErrors, som kan hända vid uppdatering av sökcachen i närvaro av sammansatta kolumner. Uppdatering av sökcachen innehåller ett exklusivt lås, men sökning i en sammansatt kolumn innebär att fältvärden läses via ProxyMetadata som försöker få ett delat lås. Det kan finnas andra scenarier som utlöser detta också.

Den här egenskapen returnerar ett nytt låsobjekt på varje åtkomst. Detta låsobjekt är inte rekursivt (för prestanda) och får endast användas i ett med satser som with cache.safe_read_lock: annars kommer dåliga saker att hända.

save_original_format(book_id, fmt)[source]

Spara en kopia i det angivna formatet som ORIGINAL_FORMAT, skriver över alla befintliga ORIGINAL_FORMAT.

search(query, restriction='', virtual_fields=None, book_ids=None)[source]

Sök i databasen efter den angivna frågan och returnera en uppsättning matchade bok-id.

Parametrar:

- restriction – En begränsning som är ANDad med angiven förfrågan. Observera att restriktioner lagras i cache, alltså sökandet efter a AND b kommer att vara långsammare än a med restriktion b.

- virtual_fields – Används internt (virtuella fält som on_device ska söka över).

- book_ids – Om inte None, en uppsättning bok-id för vilka böcker kommer att sökas i stället för att söka i alla böcker.

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]

Sök i anteckningstexten med hjälp av ett FTS-index. Om frågan är tom returneras alla anteckningar.

set_conversion_options(options, fmt='PIPE')[source]

alternativ måste vara en översikt på formen {book_id:conversion_options}

set_cover(book_id_data_map)[source]

Ställ in omslaget för denna bok. Data kan antingen vara en QImage, QPixmap, filobjekt eller bytesträng. Det kan också vara None, i vilket fall eventuellt befintligt omslag tas bort.

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[source]

Ställ in värdena för fältet som anges av namn. Returnerar uppsättningen av alla bok-id som påverkades av ändringen.

Parametrar:

- book_id_to_val_map – Mappning av book_ids till värden som bör tillämpas.

- allow_case_change – Om True, i fall det rör sig om många-en eller många-många fält att ändras. Till exempel, om en bok har taggen tag1 och du anger taggen för en annan bok till Tag1 då kommer båda böckerna att ha taggen Tag1 om allow_case_change är True, annars kommer de båda ha taggen tag1.

- do_path_update – Används internt, du bör aldrig ändra det.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[source]

Ställer in länkar för postvärden i fältet. Obs: den här metoden ändrar inte värden som inte finns i value_to_link_map

Parametrar:

- field – uppslagsnamnet

- value_to_link_map – dict(field_value:link, …). Observera att detta är värden, inte fältidentiteter.

Returnerar:

böcker ändras genom att ställa in länken

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[source]

Ställ in metadata för bokens id från Metadata-objektet mi

Inställning force_changes=True tvingar set_metadata att uppdatera fält även om mi innehåller tomma värden. I det här fallet särskiljs ’None’ från ’empty’. Om mi.XXX är None, ersätts inte XXX, annars görs det. Taggar, identifierare och omslagsattribut är specialfall. Taggar och identifierare kan inte ställas in på None så de alltid ersätts om force_changes är True. Du måste se till att mi innehåller de värden du vill att boken ska ha. Omslag ändras alltid om ett nytt omslag tillhandahålls, men tas aldrig bort. Observera också att force_changes inte har någon effekt på inställning av titel eller författare.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[source]

Ställ in anteckningsdokumentet. Om den sökbara texten skiljer sig från dokumentet, ange den som searchable_text. Om dokumentet refererar till resurser måste deras hashar finnas i resource_hashes. Ställ in remove_unused_resources till True för att rensa oanvända resurser, notera att uppdatering av en anteckning automatiskt rensar upp resurser som hör till den anteckningen ändå.

set_pref(name, val, namespace=None)[source]

Ställ in den angivna inställningen till det angivna värdet. Se också pref().

split_if_is_multiple_composite(f, val)[source]

Om f är en sammansatt kolumnsökningsnyckel och kolumnen är is_multiple, dela v i unika icke-tomma värden. Jämförelsen är skiftlägeskänslig. Ordningen är inte bevarad. Returnera en list() för kompatibilitet med proxymetadatafältgetters, till exempel taggar.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[source]

Returnera id av alla böcker som har taggen tagg som är äldre än den angivna tiden. taggjämförelse är skiftlägesoberoende.

Parametrar:

- delta – Ett tidsdeltaobjekt eller None. Om None returneras alla id med taggen.

- must_have_tag – Om inte None listan av träffar kommer begränsas till böcker som har denna tagg

- must_have_authors – En lista med författare. Om inte None listan av träffar kommer begränsas till böcker som har dessa författare (skiftlägesokänslig).

unretire_note_for(field, item_id) → int[source]

Ta bort en tidigare tillbakadragen anteckning för den angivna posten. Anteckningar tas bort när en post tas bort från databasen

user_categories_for_books(book_ids, proxy_metadata_map=None)[source]

Returnera användarkategorierna för de angivna böckerna. proxy_metadata_map är valfritt och är användbart för en prestandahöjning, i sammanhang där ett ProxyMetadata-objekt för böckerna redan finns. Det bör vara en mappning av book_ids till deras motsvarande ProxyMetadata-objekt.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Inrätta en calibre-utvecklingsmiljö »

- API-dokumentation för e-bokredigeringsverktygen

API-dokumentation för e-bokredigeringsverktygen

E-bokredigeringsverktygen består av calibre.ebooks.oeb.polish.container.Container poster som representerar en bok som en samling av HTML + resursfiler, samt olika verktyg som kan användas för att utföra operationer på behållaren. Alla verktyg är i form av modulnivåsfunktioner i de olika calibre.ebooks.oeb.polish.* modulerna.

Du erhåller ett behållarobjekt för en bok på en sökväg så här:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

Om du skriver en insticksmodul för E-bokredigeraren, får du den aktuella behållaren för boken som redigeras så här:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
report_error # No book has been opened yet

Behållarobjektet
class calibre.ebooks.oeb.polish.container.Container(rootpath, opfpath, log, clone_data=None)[source]

En behållare representerar en öppen e-bok som en mapp full av filer och en OPF-fil. Det finns två viktiga begrepp:

- Rotmappen. Detta är basen för e-boken. Alla e-böcker finns i denna mapp eller i dess undermappar.

- Namn: Dessa är sökvägar till böckernas filerna relativt rotmappen. De innehåller alltid POSIX-avskiljare och är utan citattecken. De kan ses som kanoniska identifierare för filer i boken. De flesta metoder på behållarobjektet arbetar med namn. Namn är alltid i NFC Unicode-normalformen.

- Kloner: behållarobjektet stödjer effektiva on-disk kloning, som används för att implementera kontrollpunkter i e-bokredigeraren. För att göra detta arbete, bör du aldrig komma åt filer på filsystemet direkt. Istället används raw_data() eller :meth:` open` att läsa / skriva till komponentfiler i boken.

När du konverterar mellan hrefs och namn använd de metoder som denna klass tillhandahåller, de antar att alla hrefs är i citattecken.

abspath_to_name(fullpath, root=None)[source]

Konvertera en absolut sökväg till en kanoniskt namn i förhållande till root

Parametrar:

root – Grundmappen. Som standard används roten till detta behållarobjekt.

add_file(name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None)[source]

Lägg till en fil i denna behållare. Poster för filen skapas automatiskt i OPF-manifestet och bokryggen (om filen är ett textdokument)

add_name_to_manifest(name, process_manifest_item=None)[source]

Lägg till en post i manifestet för en fil med angivna namnet. Returnerar manifest-id.

add_properties(name, *properties)[source]

Lägg specificerade egenskaperna i manifestposten som identifierats med namn.

apply_unique_properties(name, *properties)[source]

Se till att de angivna egenskaperna är inställt på endast manifestposten som identifierats med ett namn. Du kan använda None som namn för att ta bort egenskapen från alla post.

book_type = 'oeb'

Typen av bok (epub för EPUB-filer och azw3 för AZW3-filer)

commit(outpath=None, keep_parsed=False)[source]

Begå alla smutsiga analyserade objekt till filsystemet och skriva ut ebook filen på utsökväg.

Parametrar:

- output – Sökvägen att skriva den sparade e-bokfilen till. Om Ingen, används sökvägen till den ursprungliga bokfilen.

- keep_parsed – Om True kommer analyserade representationer av engagerade poster hållas i cache.

commit_item(name, keep_parsed=False)[source]

Överlämnar ett analyserat objekt till disken (det serialiseras och skrivs till den underliggande filen). Om keep_parsed är True behålls den analyserade representationen i cachen. Se även: parsed()

dirty(name)[source]

Markera det analyserade objektet som motsvarar namnet som smutsigt. Se även: parsed().

exists(name)[source]

True om en fil/mapp som motsvarar det kanoniska namn som finns. Observera att denna funktion lider av begränsningarna i det underliggande operativsystemets filsystemet, i särskilda fall (i) känsligheten. Så ifall okänsligt filsystem kommer tillbaka gäller även om det rör sig om namn skiljer sig från fallet med den underliggande filsystemet filen. Se även has_name()

filesize(name)[source]

Returnera storlek i byte av filen som representeras av det angivna kanoniska namnet. Hanterar automatiskt smutsiga analyserade objekt. Se även: analyserade()

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[source]

Lägg till en post i manifestet med href härledd från det angivna namnet. Säkerställer unika href och id automatiskt. Returnerar skapad post.

get_file_path_for_processing(name, allow_modification=True)[source]

Liknar open() förutom att den returnerar en sökväg, istället för ett öppet filobjekt.

property guide_type_map

Mappning av guidetyp till kanoniskt namn

has_name(name)[source]

Returnera True om och endast om en fil med samma kanoniska namn som angetts finns. Till skillnad från exists() är denna metod alltid skiftlägeskänslig.

href_to_name(href, base=None)[source]

Konvertera en href (relativ till bas) till ett namn. basen måste vara ett namn eller None, i vilket fall self.root används.

insert_into_xml(parent, item, index=None)[source]

Infoga post i förälder (eller lägga till om index är None), fastställande indrag. Fungerar endast med självstängande post.

is_dir = False

Om denna container representerar en uppackad bok (en katalog)

iterlinks(name, get_line_numbers=True)[source]

Iterera över alla länkar i namnet. Om get_line_numbers är True ger det resultat i form (länk, line_number, offset). Där line_number är line_number där länken förekommer och offset är antalet tecken från början av raden. Observera att offset kan faktiskt omfatta flera rader om inte noll.

make_name_unique(name)[source]

Se till att name inte redan finns i denna bok. Om den gör det, returnera en ändrad version som inte finns.

manifest_has_name(name)[source]

Returnera True om manifestet har en post som motsvarar namnet

property manifest_id_map

Mappning av manifest-id till kanoniska namn

manifest_items_of_type(predicate)[source]

Namnen på alla manifestposter vars medietyp matcher predikat. predicate kan vara en uppsättning, en lista, en sträng eller en funktion som tar ett enda argument, som kommer att anropas med medietypen.

manifest_items_with_property(property_name)[source]

Alla uppenbara post som har den angivna egenskapen

property manifest_type_map

Mappning av manifest mediatyp till lista med canonical namn av den mediatypen
Mappning av manifest mediatyp till lista med kanoniska namn på den medietypen

property mi

Metadata från den här boken som ett metadataobjekt. Observera att det här objektet är konstruerad i farten varje gång den här egenskapen begärs, så använd den sparsamt.

name_to_abspath(name)[source]

Konvertera ett kanoniskt namn till en absolut OS-beroende sökväg

name_to_href(name, base=None)[source]

Konvertera ett namn till en href förhållande till basen, som måste vara ett namn eller None i vilket fall self.root används som bas

property names_that_must_not_be_changed

Uppsättning med namn som aldrig får byta namn. Beror på e-bokformatet.

property names_that_must_not_be_removed

Uppsättning med namn som aldrig får tas bort från behållaren. Beror på e-bokformat.

property names_that_need_not_be_manifested

Uppsättning av namn som får saknas i manifestet. Beror på e-bokformat.

open(name, mode='rb')[source]

Öppna filen som hänvisas till med namnet för direkt läsning/skrivning. Observera att detta kommer att förkasta filen om den är smutsig och ta bort den från tolkningscachen. Du måste avsluta med den här filen innan du öppnar den analyserade versionen av den igen, annars kommer dåliga saker att hända.

property opf

Den analyserad OPF-filen

opf_get_or_create(name)[source]

Bekvämlig metod för att antingen returnera första XML-element med det angivna namnet eller skapar den under opf:package element och sedan lämna tillbaka den, om den inte redan finns.

property opf_version

Den version som ställts in på OPF:s <package> elementet

property opf_version_parsed

Den version som ställts in på OPF:s <package> element som en tupel av heltal

opf_xpath(expr)[source]

Bekväm metod för att beräkna ett XPath-uttryck på OPF-filen har fördefinierade opf: och dc:namespace prefix.

parsed(name)[source]

Returnera en analyserad representation av filen som anges med namn. För HTML- och XML-filer returneras ett lxml-träd. För CSS-filer returneras en css_parser-formatmall. Observera att analyserade objekt cachelagras för prestanda. Om du gör några ändringar i det analyserade objektet måste du anropa dirty() så att behållaren vet att uppdatera cacheminnet. Se även replace().

raw_data(name, decode=True, normalize_to_nfc=True)[source]

Returnera rådata motsvarande filen som anges med namn

Parametrar:

- decode – Om True och filen har en textbaserad MIME-typ, avkoda den och returnera ett unicode-objekt i stället för råa byte.

- normalize_to_nfc – Om True är det återgivna unicode-objektet normaliserat till NFC-normalformen som krävs för filformaten EPUB och AZW3.

relpath(path, base=None)[source]

Konvertera en absolut sökväg (med os avskiljare) till en sökväg i förhållande till basen (standard att self.root). Den relativa sökvägen är inte ett namn. Använd abspath_to_name() för detta.

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[source]

Ta bort de angivna posterna (av kanoniskt namn) från bokryggen. Om remove_if_no_longer_in_spine är True tas även posten bort från boken, inte bara från bokryggen.

remove_from_xml(item)[source]

Tar bort posten från förälder, rättar till indrag (fungerar bara med självstängande poster)

remove_item(name, remove_from_guide=True)[source]

Ta bort posten som identifieras med namn från den här behållaren. Detta tar bort alla referenser till posten i OPF-manifestet, guiden och ryggrraden samt från eventuella interna cacher.

rename(current_name, new_name)[source]

Byter namn på en fil från current_name till new_name. Den omställer automatiskt alla länkar inuti filen om den mapp filen är i ändras. Observera dock att länkar inte uppdateras i andra filer som kan referera till den här filen. Detta är för prestanda, sådana uppdateringar bör göras en gång, i grupp.

replace(name, obj)[source]

Ersätt det analyserade objektet som motsvarar namnet med obj, som måste vara ett liknande objekt, dvs. ett lxml-träd för HTML/XML eller en css_parser-formatmall för en CSS-fil.

replace_links(name, replace_func)[source]

Ersätt alla länkar i namn med replace_func, som måste vara en anropsbar som accepterar en URL och returnerar den ersatta URL:en. Den måste också ha ett ”ersatt” attribut som är satt till True om någon faktisk ersättning görs. Praktiska sätt att skapa sådana anropsbara är att använda klasserna LinkReplacer och LinkRebaser.

serialize_item(name)[source]

Konvertera en analyserat objekt (identifieras av kanoniskt namn) i en oktettsträng. Se parsed().

set_spine(spine_items)[source]

Anpassa bokryggen att vara spine_items där spine_items visas som en iterabel av formen (name, linear). Kommer ge ett felmeddelande om ett av namnen finns inte i manifestet.

property spine_items

En iterator som ger sökväg för varje del i bokryggar. Se även: attr:spine_iter och spine_items.

property spine_iter

En iterator som ger posten name is_linear för varje post i bokryggen. Posten är elementet lxml, som har det kanoniska filnamnet och is_linear är True om posten är linjär. Se även: spine_names och spine_items.

property spine_names

En iterator som ger name och is_linear för varje post i bokens rygg. Se även: spine_iter and spine_items.

Hantera komponentfiler i en behållare

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[source]

Ersätt länkar till filer i behållaren. Kommer iterera över alla filer i behållaren och ändra de angivna länkar i dem.

Parametrar:

- link_map – En mappning av kanoniska namn till nya kanoniska namn. Till exempel: {'images/old.png': 'images/new.png'}

- frag_map – Ett anropbart som tar två argument (name, anchor) och returnerar ett nytt ankare. Detta är användbart om du behöver ändra ankare i HTML-filer. Som standard gör det ingenting.

- replace_in_opf – Om False, ersätts inte länkar i OPF-filen.

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[source]

Byt namn på filer i behållaren, uppdaterar automatiskt alla länkar till dem.

Parametrar:

file_map – En mappning av gammalt kanoniskt namn till nytt kanoniskt namn, till exempel: {'text/kapitel 1.html': 'kapitel 1.html'}.

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[source]

Returnera mapparna som rekommenderas för de givna filnamnen. Rekommendationen är baserad på var majoriteten av filer av samma typ finns i behållaren. Om inga filer av en viss typ är närvarande, antas den rekommenderade mappen vara den mapp som innehåller OPF-filen.

Fin utskrift och automatisk reparation av tolkningsfel

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[source]

Åtgärda eventuella tolkningsfel i HTML representeras som en råsträng. Åtgärd görs med HTML5 tolkningsalgoritmen.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[source]

Åtgärda eventuella tolkningsfel i alla HTML-filer i containern. Åtgärd görs med HTML5 tolkningsalgoritmen.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[source]

Fin utskrift HTML representeras som en råsträng

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[source]

Fin utskrift CSS representerad som en råsträng

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[source]

Fin utskrift XML representeras som en råsträng. Om name är namnet på OPF, är extra OPF-specifik försköning utförs.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[source]

Fin utskrift för alla HTML/CSS/XML-filer i behållaren

Hantera bokomslag

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[source]

Ta bort ett befintligt skal, om något. Returnerar False om inget befintligt skal hittades.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[source]

Antingen skapa ett nytt skal från bokens metadata eller ersätta en befintligt skal. Returnerar True om ett befintligt omslag ersattes.

Delning och sammanslagning av filer

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[source]

Dela upp filen som anges med namn vid den position som anges av loc_or_xpath. Uppdelning migrerar automatiskt alla länkar och referenser till de berörda filerna.

Parametrar:

- loc_or_xpath – Bör vara ett XPath-uttryck som //h:div[@id=”split_here”]. Kan också vara en loc som används internt för att genomföra uppdelningen i förhandsvisningspanelen.

- before – Om True sker uppdelningen innan det identifierade elementet annars efter det.

- totals – Används internt

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[source]

Dela den angivna filen på flera platser (alla taggar som matchar det angivna XPath-uttrycket). Se också: split(). Delning migrerar automatiskt alla länkar och referenser till de berörda filerna.

Parametrar:

before – Om True sker uppdelningar innan det identifierade elementet annars efter det.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[source]

Slå samman de angivna filerna till en enda fil, migrera alla länkar och referenser till de berörda filerna automatiskt. Alla filer måste vara antingen HTML- eller CSS-filer.

Parametrar:

- category – Måste vara antingen 'text' för HTML-filer eller 'styles' för CSS-filer

- names – Listan med filer som ska slås samman

- master – Vilken av de sammanslagna filer är huvud-fil, som är den fil som kommer att finnas kvar efter sammanslagning.

Hanterar omslag

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[source]

Ställ in omslaget på boken till den bild som hänvisas till av cover_path.

Parametrar:

- cover_path – Antingen den absoluta sökvägen till en bildfil eller kanoniska namn på en bild i boken. När du använder en bild i boken måste du också ställa in alternativ, se nedan.

- report – En valfri anropningsbar som tar ett enda argument. Den kommer att anropas med information om de uppgifter som behandlas.

- options – Ingen eller en ordbok som kontrollerar hur omslaget ställs in. Ordboken kan innehålla poster: keep_aspect: True eller False (bevara omslagsbildförhållanden i EPUB) no_svg: True eller False (använd ett SVG-omslagshölje i EPUB-titelsidan) befintliga: True eller False (cover_path refererar till en befintlig bild i boken)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[source]

Markera den angivna bilden som omslagsbild.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[source]

Markera den angivna HTML-filen som titelsidan för EPUB:en.

Parametrar:

move_to_start – Om True kommer HTML-filen flyttas till början av bokryggen

Att arbeta med CSS

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[source]

Ändra en teckensnittsfamilj från old_name att NEW_NAME. Ändrar alla förekomster av teckensnitt i formatmallar, formattagg och formatattribut. Om old_name avser ett inbäddat teckensnitt, tas det bort. Du kan ställa in NEW_NAME till None för att ta bort teckensnittsfamiljen istället för att ändra den.

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]

Ta bort alla oanvända CSS-regler från boken. En oanvänd CSS-regel är en som inte matchar något faktiskt innehåll.

Parametrar:

- report – En valfri anropningsbar som tar ett enda argument. Den anropas med information om de operationer som utförs.

- remove_unused_classes – Om True tas klassattribut i HTML som inte matchar några CSS-regler också bort.

- merge_rules – Om True, slå samman regler med identiska väljare.

- merge_rules_with_identical_properties – Om True, slå samman regler med identiska egenskaper.

- remove_unreferenced_sheets – Om True, tas formatmallar som inte refereras av något innehåll bort

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[source]

Ta bort de angivna CSS-egenskaperna från alla CSS-regler i boken.

Parametrar:

- properties – Uppsättning av egenskaper för att ta bort. Till exempel: {'font-family', 'color'}.

- names – Filerna från vilka egenskaper ska tas bort. Standardvärden för alla HTML- och CSS-filer i boken.

Arbeta med innehållsförteckning

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[source]

Skapa en innehållsförteckning från en lista med XPath-uttryck. Varje uttryck i listan motsvarar en nivå av den skapade innehållsförteckningen. Till exempel: ['//h:h1', '//h:h2', '//h:h3'] kommer att skapa en trenivåers innehållsförteckning från <h1>, <h2> and <h3> taggar.

calibre.ebooks.oeb.polish.toc.from_links(container)[source]

Skapa innehållsförteckning från länkar i boken.

calibre.ebooks.oeb.polish.toc.from_files(container)[source]

Skapa innehållsförteckning från filer i boken.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[source]

Skapa en indragsinnehållsförteckning (HTML) från en befintlig NCX-innehållsförteckning.

Parametrar:

title – Titel för denna innehållsförteckning.

Bokredigeringsverktyg
class calibre.gui2.tweak_book.plugin.Tool[source]

Grunder: object

Basklass för enskilda verktyg i ett bokredigering insticksmodul. Användbara medlemmar är:

- self.plugin: En referens till: calibre.customize.Plugin objekt till vilket detta verktyg tillhör.

- self. boss

- self. gui

Metoder som måste bli ersatta i underklasser:

- create_action()

- register_shortcut()

name = None

Ändra detta till ett unikt namn det kommer att användas som en nyckel

allowed_in_toolbar = True

Om True användaren kan välja att placera detta verktyg i insticksmodul verktygsfältet

allowed_in_menu = True

Om True användaren kan välja att placera detta verktyg i insticksmodul menyn

toolbar_button_popup_mode = 'delayed'

Rullgardinsläget för menyn (om någon) på knappen i verktygsfältet. Möjliga värden är ”fördröjt”, ”snabb”, ”knappen”

property boss

calibre.gui2.tweak_book.boss.Boss objektet. Används för att anpassa användargränssnittet.

property gui

Huvudfönstret i användargränssnittet

property current_container

Returnera det aktuella objektet calibre.ebooks.oeb.polish.container.Container som representerar boken som redigeras.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[source]

Registrera en genväg som utlöser den angivna qaction. Denna genväg blir automatiskt anpassningsbar av användaren i avsnittet Tangentbordsgenvägar i redigerarens inställningar.

Parametrar:

- qaction – Ett QAction-objekt kommer att utlösas när den anpassade tangentkombinationen trycks av användaren.

- unique_name – Ett unikt namn för genvägen/åtgärden. Den kommer att användas internt, den får inte delas av några andra åtgärder i denna insticksmodul.

- default_keys – En lista över standard tangentbordsgenvägar. Om det inte anges kommer inga standardgenvägar att fastställas. Om genvägarna som anges här är i konflikt med antingen inbyggda genvägar eller genvägar från användarkonfigurations/andra insticksmoduler, kommer de att ignoreras. I så fall måste användarna anpassa genvägarna manuellt via Inställningar. Till exempel: default_keys=('Ctrl+J', 'F9').

- short_text – En valfri kort beskrivning av denna åtgärd. Om inte angivet kommer texten från QAction att användas.

- description – En valfri längre beskrivning av denna åtgärd, det kommer att användas i inställningsposten för den här genvägen.

create_action(for_toolbar=True)[source]

Skapa en QAction som kommer att läggas till antingen i insticksmodulsverktygsfältet eller insticksmodulsmenyn beroende på for_toolbar. Till exempel:

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

Se även

Metod register_shortcut().

Anpassa redigerarens användargränssnitt

E-bokredigerarens användargränssnitt bestäms av ett enda global Boss objekt. Detta har många användbara metoder som kan användas i insticksmodul kod för att utföra vanliga uppgifter.

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[source]

add_savepoint(msg)[source]

Skapa en återställningskontrollpunkt med det namn som anges som msg

apply_container_update_to_gui(mark_as_modified=True)[source]

Uppdatera alla komponenter i användargränssnittet för att återspegla de senaste data i den aktuella bokbehållaren.

Parametrar:

mark_as_modified – Om True kommer boken att markeras som ändrad, så användaren kommer att uppmanas att spara den vid avslutning.

close_editor(name)[source]

Stäng redigeraren som redigerar filen som anges av name

commit_all_editors_to_container()[source]

Utför eventuella ändringar som användaren har gjort i filer öppnas i redigeraren till behållaren. Du bör anropa den här metoden innan du utför några åtgärder på den aktuella behållaren

property currently_editing

Returnera namnet på filen som för närvarande redigeras eller None om ingen fil redigeras

edit_file(name, syntax=None, use_template=None)[source]

Öppna filen som namnges i en redigerare

Parametrar:

- syntax – Mediatypen för filen, till exempel, 'text/html'. Om inget angivits uppskattas den från filändelsen.

- use_template – En mall för att initiera den öppnade redigeraren med

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[source]

Öppna e-bok på sökväg för redigering. Kommer att visa ett fel om e-boken inte är i ett format som stöds eller den aktuella boken har osparade ändringar.

Parametrar:

edit_file – Namnet på en fil inne i den nyöppnade boken för att börja redigera. Kan också vara en lista med namn.

rewind_savepoint()[source]

Ångra föregående skapande en återställningskontrollpunkt, användbart om du skapar en kontrollpunkt, sedan avbryta med inga ändringar

save_book()[source]

Spara boken. Sparning utförs i bakgrunden

set_modified()[source]

Markera boken som har modifierats

show_current_diff(allow_revert=True, to_container=None)[source]

Visa ändringarna i boken från dess senaste kontrollpunktstillstånd

Parametrar:

- allow_revert – Om True kommer skillnadsdialogrutan att ha en knapp som låter användaren återställa alla ändringar

- to_container – En behållarobjekt för att jämföra den aktuella behållaren till. Om None är det tidigare kontrollspunktsbehållare som används

show_editor(name)[source]

Visa redigeraren som redigerar filen som anges med name

sync_preview_to_editor()[source]

Synkronisera position av förhandsvisningspanelen till den aktuella markörpositionen i den aktuella redigeraren

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- nästa |
- föregående |
- Start »
- Digital Rights Management (DRM)

Digital Rights Management (DRM)

Digital Rights Management (DRM) är en generisk term för åtkomstkontrollteknik som kan användas av hårdvarutillverkare, utgivare, upphovsrättsinnehavare och individer för att försöka införa begränsningar för användningen av digitalt innehåll och enheter. Det beskrivs också, ibland, nedsättande som Digital Restrictions Management. Termen används för att beskriva all teknik som hämmar användningar (legitima eller på andra sätt) av digitalt innehåll som inte önskas eller förutsågs av innehållsleverantören. Termen brukar inte hänvisa till andra former av kopieringsskydd som kan kringgås utan att ändra filen eller enheten, såsom serienummer eller nyckelfiler. Det kan också hänvisa till begränsningar i samband med specifika fall av digitala verk eller enheter. DRM-tekniker försöker kontrollera användningen av digitala medier genom att förhindra åtkomst, kopiering eller konvertering till andra format av slutanvändare. Se Wikipedia [https://sv.wikipedia.org/wiki/Digital_rights_management].

Vad innebär DRM för mig personligen?

När du köper en e-bok med DRM äger du den inte riktigt utan har köpt tillstånd att använda den på ett sätt som säljaren dikterar för dig. DRM begränsar vad du kan göra med e-böcker du har ”köpt”. Ofta är personer som köper böcker med DRM omedvetna om omfattningen av dessa begränsningar. Dessa begränsningar hindrar dig från att formatera om e-boken efter eget tycke, inklusive att göra stilistiska ändringar som att justera teckensnittsstorlekarna, även om det finns programvara som ger dig möjlighet att göra sådana saker för icke DRM-böcker. Folk blir ofta förvånade över att en e-bok som de har köpt i ett visst format inte kan konverteras till ett annat format om e-boken har DRM. Så om du har en Amazon Kindle och köper en bok som säljs av Barnes and Nobles, bör du veta att om den e-boken har DRM kommer du inte att kunna läsa den på din Kindle. Lägg märke till att jag talar om en bok du köper, inte stjäl eller piratkopierar utan KÖPER.

Vad gör DRM för författare?

Utgivare av e-böcker med DRM hävdar att DRM är allt för författarnas skull och för att skydda deras konstnärliga integritet och förhindra piratkopiering. Men DRM hindrar inte piratkopiering. Människor som vill piratkopiera innehåll eller använda piratkopierat innehåll gör det fortfarande och lyckas. De tre stora DRM-systemen för e-böcker idag drivs av Amazon, Adobe och Barnes and Noble och alla tre DRM-system har knäckts. Allt DRM gör är besvärar legitima användare. Det kan hävdas att det faktiskt skadar författare eftersom personer som skulle ha köpt boken väljer att hitta en piratkopierad version eftersom de inte är villiga att stå ut med DRM. De som skulle piratkopiera i frånvaro av DRM gör det också i sin närvaro. För att upprepa är huvudpunkten att DRM inte hindrar piratkopiering. Så DRM är inte bara meningslöst och skadligt för köpare av e-böcker utan också ett slöseri med pengar.

DRM och frihet

Även om digitalt innehåll kan användas för att göra såväl information och kreativa verk lättillgängliga för alla och stärka mänskligheten, ligger detta inte i intresset för vissa utgivare som vill styra människor bort från den här möjligheten till frihet helt enkelt för att behålla sin relevans i världsutvecklingen, så snabbt att de inte kan hänga med.

Varför stöder calibre inte DRM?

calibre är programvara med öppen källkod medan DRM till sin natur är stängd. Om calibre skulle stöda öppning eller visning av DRM-filer kan det vara trivialt modifierat för att användas som ett verktyg för DRM-borttagning, vilket är olagligt enligt dagens lagar. Programvara med öppen källkod och DRM är en konflikt mellan principer. Medan DRM handlar om att styra användaren, handlar programvara med öppen källkod om att ge användaren möjlighet. De två kan helt enkelt inte samexistera.

Vad är calibres syn på innehållsleverantörer?

Vi är övertygade om att författare och andra innehållsleverantörer ska kompenseras för sina ansträngningar, men DRM är inte rätt väg att ta. Vi utvecklar denna databas med DRM-fria e-böcker från olika källor för att hjälpa dig att hitta DRM-fria alternativ och för att hjälpa oberoende författare och utgivare av DRM-fria e-böcker att publicera sitt innehåll. Vi hoppas att du kommer att finna detta användbart och vi ber att du inte piratkopierar innehållet som görs tillgängligt för dig här.

Hur kan jag hjälpa till att bekämpa DRM?

Som någon som läser och köper e-böcker kan du hjälpa till att bekämpa DRM. Köp inte e-böcker med DRM. Det finns några utgivare som publicerar DRM-fria e-böcker. Ansträng dig för att se om de har e-boken du letar efter. Om du gillar böcker av vissa oberoende författare som säljer DRM-fria e-böcker och du har råd att göra donationer till dem. Detta är väl använda pengar eftersom deras e-böcker tenderar att vara billigare (det kan finnas undantag) än de du skulle köpa från utgivare av böcker med DRM och förmodligen skulle fungera på alla enheter du äger i framtiden, vilket sparar dig kostnaden för att köpa e-boken igen. Avskräck inte utgivare och författare av DRM-fria e-böcker genom att piratkopiera deras innehåll. Innehållsleverantörer förtjänar kompensation för sina ansträngningar. Bestraffa dem inte för att försöka göra din läsupplevelse bättre genom att göra DRM-fria e-böcker tillgängliga. I det långa loppet är detta skadligt för dig. Om du har köpt böcker från säljare som har både böcker med DRM och utan DRM, utan att veta om de har DRM eller inte, gör det till en poäng att lämna en kommentar eller recension på webbplatsen och informera framtida köpare om dess DRM-status. Många säljare tycker inte att det är viktigt att tydligt ange för sina köpare om en e-bok har DRM eller inte. Här [https://www.defectivebydesign.org/guide/ebooks] hittar du en guide till en DRM-fri livsstil.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- föregående |
- Start »
- Ordlista

Ordlista

RSS

RSS (Really Simple Syndication) är en webbutfodringsformat som används för att publicera ofta uppdaterat innehåll, som nyhetsartiklar, blogginlägg o.s.v. Det är ett format som är särskilt lämpad för att läsas av datorer, och är därför det bästa sättet att få innehåll från webben till en e-bok. Det finns många andra utfodringsformat som används på internet, och calibre förstår de flesta av dem. Framför allt har det bra stöd för ATOM format, som ofta används för bloggar.

recept

Ett recept är en uppsättning instruktioner som lär calibre hur man konverterar en nyhetskälla på nätet, till exempel en tidning eller en blogg, till en e-bok. Ett recept är i huvudsak Python [https://www.python.org] code. Som sådant är det i stånd att konvertera godtyckligt komplexa nyhetskällor i böcker. På den enklaste nivån, är det bara en uppsättning variabler, såsom URL:er, som ger calibre tillräckligt med information för att gå ut på Internet och hämta nyheterna.

HTML

HTML (Hyper Text Markup Language), en delmängd av Standard Generalized Markup Language (SGML) för elektronisk publicering, är den specifika standarden som används för World Wide Web.

CSS

CSS (Cascading Style Sheets) är ett språk som används för att beskriva hur en HTML dokument bör återges (visuell design).

API

API (Application Programming Interface) är ett källkodsgränssnitt som ett bibliotek tillhandahåller för att stöda begäran från tjänster av datorprogram.

LRF

LRF E-bokformat som läses av SONY e-bokläsenheter.

URL

URL (Uniform Resource Locator) till exempel: http://example.com

regexp

Reguljära uttryck tillhandahåller ett kortfattad och flexibelt sätt att identifiera textsträngar av intresse, som särskilda tecken, ord eller teckenmönster. Se handledningen för en introduktion till reguljära uttryck.

© Copyright Kovid Goyal.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Python Modulindex

Python Modulindex
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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Översikt: modulkällkod

Alla moduler där källkod finns

- 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.customize

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.customize »
- calibre.customize.conversion

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.db.cache

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.devices.interface

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.devices.usbms.cli

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.devices.usbms.device

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.devices.usbms.driver

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.metadata.book.base

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.metadata.sources.base

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.container

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.cover

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.css

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.fonts

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.jacket

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.pretty

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.replace

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.split

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.ebooks.oeb.polish.toc

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.gui2.actions

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.gui2.preferences

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.gui2.tweak_book.boss

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.gui2.tweak_book.plugin

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.utils.formatter_functions

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Navigation

- moduler
- Start »
- Modulkällkod »
- calibre.web.feeds.news

Källkod för 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.
Senast uppdaterad den maj 03, 2024.

Table of Contents

Användarmanualen för calibre

Det grafiska användargränssnittet

1. Åtgärder

1. Lägg till böcker
2. Redigera metadata
3. Konvertera böcker
4. Visa
5. Överför till enhet
6. Hämta nyheter
7. Bibliotek
8. Enhet
9. Spara till disk
10. Ansluta/dela
11. Ta bort böcker

2. Inställningar
3. Kataloger
4. Söka & sortera
5. Sökgränssnittet
6. Spara sökningar
7. Sökning i hela texten i alla böcker
8. Virtuella bibliotek
9. Tillfällig markering av böcker
10. Uppskatta metadata från filnamn
11. Bokdetaljer
12. Taggbläddrare
13. Omslagsrutnät
14. Omslagsbläddrare
15. Adding notes for authors, series, etc.
16. Snabbvisning
17. Jobb
18. Tangentbordsgenvägar

Lägga till din favorit nyhetswebbplats

1. Helt automatisk hämtning

1. calibre-bloggen

2. bbc.co.uk

2. Anpassa hämtningsprocessen

1. Använda den tryckta versionen av bbc.co.uk
2. Ersätta artikelformat
3. Skivning och styckning i tärningar
4. Exempel i verkligheten

3. Tips för att utveckla nya recept

4. Ytterligare läsning

5. API-dokumentation

1. API-dokumentation för recept

1. BasicNewsRecipe

E-bokvisaren

1. Starta e-bokvisaren

2. Navigera omkring i en e-bok

1. Bokmärken
2. Innehållsförteckning
3. Navigering efter plats
4. Referensläge

3. Markera text
4. Högläsning
5. Söka i texten
6. Följa länkar med endast tangentbordet

7. Anpassa utseendet och känslan av din läsupplevelse

8. Ordboksuppslagning
9. Kopiera text och bilder
10. Zooma in på bilder
11. Tangentbords- genvägar
12. Inte återflödbart innehåll

13. Designa din bok för att fungera bra med calibre-visaren

Konvertering av e-bok

1. Introduktion

2. Utseende & känsla

1. Teckensnitt
2. Text
3. Layout
4. Formgivning
5. Omvandla format
6. Omvandla HTML

3. Sidinställning
4. Heuristisk bearbetning
5. Söka & ersätta

6. Struktrurdetektering

1. Kapitel och sidbrytningar

2. Diverse

7. Innehållsförteckning

8. Använda bilder som kapitelrubriker vid konvertering av HTML-inmatningsdokument

9. Använda taggattribut för att leverera texten för posterna i innehållsförteckningen

10. Hur alternativ ställs in/sparas för konvertering

11. Formatspecifika tips

1. Konvertera Microsoft Word-dokument

1. Äldre .doc-filer

2. Konvertera TXT-dokument
3. Konvertera PDF-dokument
4. Serietidningssamlingar

5. Demonstration av avancerad EPUB-formatering

6. Konvertera ODT-dokument

7. Konvertera till PDF

1. Sidhuvuden och sidfötter

2. Utskrivbar innehållsförteckning

3. Anpassade sidmarginaler för enskilda HTML-filer

Redigera e-böcker

Grundläggande arbetsflöde

Filhanteraren

1. Byta namn på filer
2. Slå samman filer
3. Ändra textfilordning
4. Markera omslaget
5. Ta bort filer
6. Exportera filer

7. Lägga till nya bilder/teckensnitt/osv. eller skapa nya tomma filer

8. Ersätta filer

9. Länka formatmallar till HTML-filer effektivt

Söka & ersätta

1. Sparade sökningar
2. Funktionsläge
3. Sök ignorerar HTML-taggar

Automatiserade verktyg

1. Redigera innehållsförteckningen
2. Kontrollera boken
3. Lägga till ett omslag
4. Bädda in refererade teckensnitt
5. Underuppsätta inbäddade teckensnitt
6. Förbättra skiljeteckenhantering
7. Omvandla CSS-egenskaper
8. Ta bort oanvända CSS-regler
9. Justera HTML
10. Försköna filer
11. Infoga en indragsinnehållsförteckning
12. Ställa in semantik
13. Filtrera formatinformation
14. Uppgraderar bokens inre delar

Kontrollpunkter

Panelen för realtidsförhandsvisning

1. Dela upp HTML-filer

CSS-realtidspanelen

Diverse verktyg

1. Innehållsförteckningsvyn

2. Kontrollera stavning av ord i boken

1. Lägga till nya ordböcker

3. Infoga specialtecken
4. Kodinspektörsvyn
5. Kontrollera externa länkar
6. Hämta externa resurser
7. Ordna filer i mappar efter typ

8. Importera filer i andra e-bokformat som EPUB

9. Rapportverktyget

Speciella funktioner i kodredigeraren

1. Syntaxmarkering
2. Kontextkänslig hjälp
3. Automatisk komplettering
4. Kodavsnitt

calibre-innehållsservern

1. Åtkomst till innehållsservern från andra enheter

1. Åtkomst till servern från enheter i ditt hemnätverk

1. Problemlösning av hemnätverksanslutningen

2. Åtkomst till servern från var som helst på internet

2. Servergränssnittet

1. Boklistan

2. Bokläsaren

3. Webbläsarstöd

4. Aktivera frånkopplat stöd

5. Hantera användarkonton endast från kommandoraden

6. Integrera calibre-innehållsservern med andra servrar

1. Använda en fullständig virtuell värd

2. Använda ett URL-prefix

7. Skapa en tjänst för calibre-servern på ett modernt Linux-system

Jämföra e-böcker

1. Förstå jämförelsevyn

2. Starta jämförelseverktyget

1. Jämför två e-bokfiler

2. Jämföra ORIGINAL_FMT med FMT

3. Jämföra en kontrollpunkt med bokens aktuella tillstånd under redigering

Redigera metadata för e-böcker

1. Redigera metadata för en bok i taget

1. Hämta metadata
2. Hantera bokformat
3. Allt om omslag

2. Redigera metadata för många böcker samtidigt

1. Sök och ersätt

2. Masshämtning av metadata

3. Lägga till extra datafiler i en bok

Vanliga frågor

Konvertering av e-bokformat

1. Vilka format stöder calibre-konvertering till/från?

2. Vilka är de bästa källformaten att konvertera?

3. Jag konverterade en PDF-fil, men resultatet har diverse problem?

4. Hur konverterar jag min fil som innehåller icke-engelska tecken eller typografiska citattecken?

5. Vad är det med innehållsförteckningar i MOBI-filer?

6. Hur konverterar jag en samling HTML-filer i en viss ordning?

7. EPUB:en som producerades med calibre är inte giltig?

8. Hur använder jag de avancerade funktionerna i konverteringsverktyget?

Enhetsintergration

1. Vilka enheter stöder calibre?

2. Hur kan jag hjälpa till för att min enhet ska få stöd i calibre?

3. Min enhet upptäcks inte av calibre?

4. Min enhet är inte standard eller ovanlig. Vad kan jag göra för att ansluta till den?

5. Hur använder jag calibre med min iPad/iPhone/iPod touch?

1. Använda innehållsservern

6. Hur använder jag calibre med min Android-telefon/surfplatta eller Kindle Fire?

1. Med hjälp av en USB-kabel

2. Över luften

7. Kan jag komma åt mina calibre-böcker med hjälp av webbläsaren i min Kindle eller annan läsenhet?

8. Jag kan inte sända e-post med hjälp av calibre?

9. Min enhet monteras som skrivskyddad i Linux, så calibre kan inte ansluta till den?

10. Varför stöder inte calibre samlingar på Kindle eller hyllor på Nook?

11. Jag får ett felmeddelande när jag försöker använda calibre med min Kobo Touch/Glo/osv.?

12. Omslag för böcker som jag överför till min e-bläck Kindle dyker upp en stund och ersätts sedan av ett generiskt omslag?

13. Omslagen för mina MOBI-filer har slutat visas i Kindle för PC/Kindle for Android/iPad osv.

14. Jag överförde några böcker till min Kindle genom att använda calibre och de dök inte upp?

Bibliotekshantering

1. Var är bokfilerna lagrade?

2. Hur hanterar calibre författarnamn och sortering?

3. Varför låter calibre mig inte att lagra böcker i min egen mappstruktur?

4. Varför har calibre inte en kolumn för foo?

5. Kan jag ha en kolumn som visar formaten eller ISBN?

6. Hur flyttar jag min calibre-data från en dator till en annan?

7. Listan över böcker i calibre är tom!

8. Jag får felmeddelanden med mitt calibre-biblioteket på en nätverksenhet/NAS?

Diverse

1. Amazon stoppar e-postleverans av MOBI-filer?

2. Jag vill att calibre hämtar nyheter från min favorit nyhetswebbplats.

3. Varför namnet calibre?

4. Varför visar calibre bara några av mina teckensnitt på macOS?

5. calibre startar inte på Windows?

6. calibre fryser/kraschar ibland?

7. calibres e-bokvisare och redigeringsverktyget fungerar inte på Windows?

8. Användning av visaren eller att göra några konverteringar resulterar i ett felmeddelande om tillstånd nekad på Windows

9. calibre startar inte/kraschar på macOS?

10. Jag får bara en svart eller vit skärm när jag kör calibres e-bokvisare?

11. Jag hämtade installationsprogrammet, men det fungerar inte?

12. Mitt antivirusprogram påstår att calibre är ett virus/trojan?

13. Hur säkerhetskopierar jag calibre?

14. Hur använder jag köpta EPUB-böcker med calibre (eller vad ska jag göra med .acsm-filer)?

15. Jag får felmeddelandet ”Åtkomst nekad”?

16. Kan jag få kommentarmetadata att dyka upp på min läsenhet?

17. Hur får jag calibre att använda min HTTP-proxy?

18. Jag vill ha en del funktioner tillagda i calibre. Vad kan jag göra?

19. Varför har inte calibre en automatisk uppdatering?

20. Hur är calibre licensierad?

21. Hur kör jag calibre från mitt USB-minne?

22. Hur kör jag delar av calibre som nyhetshämtning och innehållsserver på min egen Linux-server?

Handledningar

Lägga till din favorit nyhetswebbplats

1. Helt automatisk hämtning

1. calibre-bloggen

2. bbc.co.uk

2. Anpassa hämtningsprocessen

1. Använda den tryckta versionen av bbc.co.uk
2. Ersätta artikelformat
3. Skivning och styckning i tärningar
4. Exempel i verkligheten

3. Tips för att utveckla nya recept

4. Ytterligare läsning

5. API-dokumentation

1. API-dokumentation för recept

Hantera undergrupper av böcker, till exempel ”genre”

1. Anpassa
2. Söka
3. Begränsningar
4. Användbara mallfunktioner

XPath-handledning

1. Välja efter taggnamn
2. Välja efter attribut
3. Välja efter tagginnehåll
4. Exempel e-bok
5. XPath inbyggda funktioner

Mallspråket för calibre

1. Grundläggande mallar

2. Avancerad formatering

3. Använda mallar för att definiera anpassade kolumner

4. Mallar och pluggbrädor

5. Using functions in templates - Single Function Mode

6. General Program Mode

7. More complex programs in template expressions - Template Program Mode

8. Python Template Mode

9. Lagrade mallar

10. Providing additional information to templates

11. Notes on the difference between modes

12. Användardefinierade Python-mallfunktioner

13. Särskilda anvisningar för att spara/skicka mallar

14. Tips

15. Funktionsreferens

1. Reference for all built-in template language functions

Allt om att använda reguljära uttryck i calibre

1. Först, ett varningens ord och ett modets ord

2. Var i calibre kan man använda reguljära uttryck?

3. Vad i hela världen är ett reguljärt uttryck?

4. Har du lust att förklara?

5. Det låter inte så illa. Vad kommer härnäst?

6. Hej, snyggt! Detta börjar låta vettigt!

7. Tja, dessa specialtecken är mycket snygga och allt, men vad händer om jag ville matcha en punkt eller ett frågetecken?

8. Så, vilka är de användbaraste uppsättningarna?

9. Men om jag hade några olika strängar jag ville matcha, blir saker komplicerade?

10. Du missade…

11. I början sa du att det fanns ett sätt att göra ett reguljärt uttryck skiftlägesokänsligt?

12. Jag tror jag börjar förstå dessa reguljära uttryck nu… hur använder jag dem i calibre?

1. Konverteringar
2. Lägga till böcker
3. Redigera metadata i grupp

13. Snabbreferens

1. Snabbreferens för regexp-syntax

14. Förtjänst

Skriv dina egna insticksmoduler för att utöka calibres funktionalitet

1. Anatomi av en calibre-insticksmodul

2. En användargränssnitts insticksmodul

1. __init__.py
2. ui.py
3. main.py
4. Få resurser från insticksmodulens ZIP-fil

5. Aktivera användarkonfiguration för din insticksmodul

3. Redigera bokinsticksmodul

1. main.py

4. Lägga till översättningar till din insticksmodul

5. Insticksmoduls API
6. Felsöka insticksmodul
7. Fler insticksmodul exempel
8. Dela dina insticksmodul med andra

Matematiktypsättning i e-böcker

1. En enkel HTML-fil med matematik

2. Mer information

Skapa kataloger för AZW3 • EPUB • MOBI

1. Välja böcker att katalogisera
2. Inkluderade avsnitt
3. Prefix
4. Exkluderade böcker
5. Exkluderade genrer
6. Andra alternativ
7. Anpassade katalogomslag
8. Ytterligare hjälpresurser

Virtuella bibliotek

1. Skapa virtuella bibliotek

1. Exempel på användbara virtuella bibliotek

2. Arbeta med virtuella bibliotek
3. Använda virtuella bibliotek i sökningar
4. Använda ytterligare begränsningar

calibre:// URL-schema

1. Byt till ett specifikt bibliotek

2. Visa en specifik bok i calibre

3. Öppna en specifik bok i e-bokvisaren på en specifik position

4. Söker efter böcker

5. Öppna ett bokdetaljfönster på en bok i något bibliotek

6. Öppna anteckningarna som är kopplade till en författare/serie/osv.

7. Hexkodning av URL-parametrar

Anpassa calibre

1. Miljövariabler
2. Justeringar
3. Åsidosätta ikoner, mallar o.s.v.
4. Skapa dina egna ikonteman för calibre
5. Anpassa calibre med insticksmoduler

Kommandoradsgränssnitt
Dokumenterade kommandon
calibre

1. [alternativ]

calibre-customize

1. [alternativ]

calibre-debug

1. [alternativ]

calibre-server

1. [alternativ]

calibre-smtp

1. [alternativ]

calibredb

1. GLOBALA ALTERNATIV
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. ALTERNATIV FÖR INMATNING
2. ALTERNATIV FÖR UTMATNING
3. UTSEENDE OCH KÄNSLA
4. HEURISTISK BEARBETNING
5. SÖK OCH ERSÄTT
6. STRUKTURDETEKTERING
7. INNEHÅLLSFÖRTECKNING
8. METADATA
9. FELSÖK

ebook-edit

1. [alternativ]

ebook-meta

1. [alternativ]

ebook-polish

1. [alternativ]

ebook-viewer

1. [alternativ]

fetch-ebook-metadata

1. [alternativ]

lrf2lrs

1. [alternativ]

lrfviewer

1. [alternativ]

lrs2lrf

1. [alternativ]

web2disk

1. [alternativ]

Odokumenterade kommandon

Inrätta en calibre-utvecklingsmiljö

1. Konstruktionsfilosofi

1. Kodutformning

2. Få koden

1. Skicka in dina ändringar som ska ingå

3. Windows-utvecklingsmiljö
4. macOS-utvecklingsmiljö
5. Linux-utvecklingsmiljö

6. Att ha separata ”normala”- och ”utvecklings”-installationer av calibre på samma dator

7. Felsökningstips

1. Använda utskriftssatser

2. Använda en interaktiv Python-tolk

3. Använda Python-felsökaren som fjärrfelsökare

4. Använda felsökaren i din favorit Python-IDE

5. Köra godtyckliga skript i Python-miljön för calibre

8. Använda calibre i dina projekt

1. Binär installation av calibre

2. Källkodsinstallation på Linux

9. API-dokumentation för olika delar av calibre

1. API-dokumentation för recept

1. BasicNewsRecipe

2. API-dokumentation för insticksmoduler

1. Insticksmoduler
2. FileTypePlugin
3. Metadatainsticksmodul
4. Katalog insticksmodul
5. Insticksmoduler för metadatahämtning
6. Konvertering insticksmodul
7. Enhetsdrivrutiner
8. Användargränssnittsåtgärder
9. Inställningar insticksmodul

3. API-dokumentation för databasgränssnittet

1. Cache

4. API-dokumentation för e-bokredigeringsverktygen

1. Behållarobjektet

2. Hantera komponentfiler i en behållare

3. Fin utskrift och automatisk reparation av tolkningsfel

4. Hantera bokomslag
5. Delning och sammanslagning av filer
6. Hanterar omslag
7. Att arbeta med CSS
8. Arbeta med innehållsförteckning
9. Bokredigeringsverktyg
10. Anpassa redigerarens användargränssnitt

Digital Rights Management (DRM)

1. Vad innebär DRM för mig personligen?

2. Vad gör DRM för författare?

3. DRM och frihet

4. Varför stöder calibre inte DRM?

5. Vad är calibres syn på innehållsleverantörer?

6. Hur kan jag hjälpa till att bekämpa DRM?

Ordlista