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

Навігація

- модулі
- наступний |
- Старт »
- Підручник користувача calibre

Підручник користувача calibre
calibre — програма для керування бібліотекою електронних книг. Вона здатна показувати, перетворювати і каталогізувати електронні книги у більшості з основних форматів електронних книг. Вона також здатна обмінюватися даними з багатьма пристроями для читання електронних книг. Програма здатна виходити до інтернету і отримувати метадані щодо ваших книг. Вона може завантажувати електронні газети і перетворювати їх на книги для зручного читання. Програма здатна працювати на багатьох програмних платформах, зокрема на Linux, Windows та macOS.

Ви щойно запустили calibre. Що слід зробити спочатку? Перш ніж calibre зможе працювати з вашими електронними книгами, програмі потрібні дані щодо них. Перетягніть і скиньте декілька файлів електронних книг до вікна calibre або натисніть кнопку «Додати книги» і вкажіть програмі файли електронних книг, з якими ви хочете попрацювати. Щойно книги буде додано, вони з’являться у списку основної панелі перегляду, десь так:

Після додавання книг до списку, ймовірно, вам захочеться якусь з них почитати. Для цього слід перетворити дані книги на формат, який може обробляти ваш пристрій для читання книг. Під час першого запуску calibre працюватиме майстер початкового налаштовування, за допомогою якого ви зможете налаштувати calibre на роботу з пристроєм для читання електронних книг. Перетворення даних книг — річ доволі проста. Достатньо позначити у списку пункт книги, яку ви хочете перетворити, а потім натиснути кнопку «Перетворити книги». Можете проігнорувати усі параметри налаштовування і просто натиснути кнопку «Гаразд». Після цього, у нижньому правому куті програми почне крутитися маленька піктограма. Щойно її обертання буде завершено, ви зможете скористатися перетвореною книгою. натисніть кнопку «Переглянути», щоб почати читати книгу.

Якщо ви хочете прочитати книгу на вашому пристрої для читання, з’єднайте його з комп’ютером, зачекайте поки calibre визначить його (10-20 секунд), тоді натисніть кнопку «Надіслати до пристрою». Щойно піктограма припинить обертатися, від’єднайте ваш пристрій та читайте! Якщо ви не перетворювали книгу на минулому кроці, calibre виконає автоматичне перетворення даних у формат, сумісний з вашим пристроєм для читання книг.

Щоб ознайомитися зі складнішими прийомами користування програмою, прочитайте розділ Графічний інтерфейс користувача. Із ще потужнішими можливостями можна ознайомитися за допомогою розділу Інтерфейс командного рядка. Також буде корисним прочитати список Поширені питання та відповіді на них.

Якщо у вас виникають додаткові питання або ви хочете обговорити calibre із іншими користувачами або попросити про допомогу щодо певних питань, скористайтеся нашим форумом та іншими допоміжними ресурсами [https://calibre-ebook.com/uk/help].

Розділи

- Графічний інтерфейс користувача
- Додавання вашого улюбленого сайта новин
- Переглядач електронних книжок
- Перетворення даних електронних книг
- Редагування електронних книг
- Сервер даних calibre
- Порівняння електронних книг
- Редагування метаданих електронної книги
- Поширені питання та відповіді на них
- Навчальні посібники
- Схема адрес calibre://
- Налаштування calibre
- Інтерфейс командного рядка

- Налаштовування середовища для розробки calibre

- Керування цифровими правами (DRM)

- Глосарій

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Графічний інтерфейс користувача

Графічний інтерфейс користувача

Графічний інтерфейс користувача надає доступ до усіх можливостей із керування бібліотекою та перетворення даних електронних книг у різні формати. Типовим початковим прийомом користування calibre є додавання книг з вашої бібліотеки на диску до вбудованої бази даних програми. Після збирання даних ви зможете виконувати над книгами різноманітні Дії, зокрема перетворення даних з одного формата на інший, перенесення книг на пристрій для читання, перегляд книг на вашому комп’ютері та редагування метаданих. Останнє включає зокрема внесення змін до зображення обкладинки, опису та міток. Зауважте, що calibre створює копії доданих вами до бази даних файлів. Початкові файли залишаються незмінними.

Інтерфейс поділений на декілька секцій:

- Дії

- Налаштування

- Каталоги

- Пошук і упорядковування

- Інтерфейс пошуку

- Збереження результатів пошуку

- Повнотекстовий пошук в усіх книгах

- Віртуальні бібліотеки

- Тимчасове позначення книг

- Визначення метаданих з назв файлів

- Подробиці щодо книги

- Переглядач міток

- Таблиця обкладинок

- Переглядач обкладинок

- Додавання нотаток щодо авторів, циклів тощо

- Швидкий перегляд

- Завдання

- Клавіатурні скорочення

Дії

На панелі інструментів програми розташовано зручні кнопки дій, за допомогою яких ви зможете отримувати доступ до типових функціональних можливостей програми. Якщо ви наведете вказівник на стрілочку праворуч від такої кнопки і клацнете лівою кнопкою миші, програма відкриє список варіантів, які є альтернативними до типової дії. Будь ласка, зауважте, що панель інструментів може виглядати у вашій системі трохи інакше — усе залежить від того, чи з’єднано з вашим комп’ютером пристрій для читання електронних книг.

- Додати книги

- Редагувати метадані

- Перетворити книги

- Перегляд

- Надіслати до пристрою

- Отримати новини

- Бібліотека

- Пристрій

- Зберегти на диск

- З’єднатись/Роздати

- Вилучити книжки

Додати книги

У дії Додати книги є сім варіантів, доступ до яких можна отримати за допомогою клацання кнопкою миші праворуч від відповідної кнопки.

1. Додати книги з однієї теки: відкриває діалогове вікно вибору файлів, за допомогою якого ви можете вказати, які книги з теки слід додати. Ця дія залежна від контексту, тобто, вона залежить від того, які каталог було вибрано. Якщо ви вибрали каталог Бібліотека, книги буде додано до бібліотеки. Якщо вами будевибрано пристрій для читання електронних книг, книги буде вивантажено на пристрій, тощо.

2. Додати книги з тек і підтек: надає вам змогу вибрати теку. Програма виконає пошук у теці і усіх її підтеках і додасть усі знайдені книги до бібліотеки. Ви можете вказати програмі, слід додавати усі файли в окремій теці до одного запису книги чи до записів декількох книг. calibre вважатиме, що у кожній теці міститься лише одна книга. Усі файли у теці вважатимуться файлами тієї самої книги у різних форматах. Ця діє є оберненою щодо Зберегти на диск, тобто ви можете виконати Зберегти на диск, вилучити книги і повторно додати їх у режимі однієї книги на теку без втрати метаданих, окрім дати додавання (якщо ви нічого не змінювали у параметрах дії «Зберегти на диск»).

3. Додати декілька книг з архіву (ZIP або RAR): надає вам змогу додати декілька електронних книг, які зберігаються у вказаних файлах ZIP або RAR. Це зручна кнопка, за допомогою якої можна уникнути початкового розпаковування архіву і додавання книг за допомогою одного із описаних вище варіантів дій.

4. Додати порожню книгу (без будь-яких форматів): надає вам змогу створити порожній запис книги. Ним згодом можна скористатися для заповнення відомостей щодо книги, якої ще немає у вашій збірці.

5. Додати з ISBN: надає змогу додати одну або декілька книг за допомогою введення їхніх кодів ISBN.

6. Додати файли до вибраних записів книг: надає вам змогу додати або оновити файли, пов’язані із наявною у вашій бібліотеці книгою.

7. Додати дані до позначених записів книг: надає вам змогу додати довільну кількість додаткових файлів, які буде збережено у підкаталозі data каталогу книги. Див. Додавання файлів даних до книги, щоб дізнатися більше.

8. Додати порожній файл до позначених записів книг: надає вам змогу додати порожній файл у вказаному форматі до позначених записів книг.

Інструмент Додати книги може читати метадані у широкому спектрі форматів електронних книг. Крім того, він намагається вгадати метадані на основі назви файла. Див. розділ Визначення метаданих з назв файлів, щоб дізнатися більше про налаштовування цієї можливості.

Щоб додати формат для наявної книги ви можете скористатися одним із трьох способів:

1. Перетягнути і скинути позначку файла на панель параметрів книги, розташовану у правій частині головного вікна програми.

2. Натиснути стрілку, праворуч від кнопки Додати книги і вибрати пункт Додати файли до вибраних записів книг.

3. Натиснути червону кнопку Додати книги у правій частині діалогового вікна Редагувати метадані. Це діалогове вікно можна відкрити за допомогою пункту Редагувати метадані.

Редагувати метадані

У дії Редагувати метадані передбачено чотири варіанти, доступ до яких можна отримати натисканням стрілки, розташованої праворуч від кнопки.

1. Редагувати метадані індивідуально: надає вам змогу редагувати метадані книг окремо із можливістю отримання метаданих, зокрема зображень обкладинок, з інтернету. Ця дія також надає змогу додавати або вилучати певні формати електронних книг до запису книги.

2. Редагувати метадані разом: надає змогу редагувати типові поля метаданих пакетно. Працює для усіх книг, якщо було позначено на панелі бібліотеки.

3. Завантажити метадані і обкладинки: отримує метадані і зображення обкладинок (якщо такі вдасться знайти) для книг, які ви позначили у списку.

4. Злити записи книг: надає вам змогу об’єднувати метадані та формати двох або декількох записів книг. Ви зможете вибрати, чи слід залишати записи, які не було спочатку позначено.

5. Керування файлами даних: керування додатковими файлами даних, які пов’язано із вибраними книгами.

Щоб дізнатися більше, див. Редагування метаданих електронної книги.

Перетворити книги

Дані електронних книг можна перетворити з поточного формату на формат, який є придатним для читання на вашому пристрої. Багато придбаних вами книжок може бути захищено засобами технології Digital Rights Management (DRM). calibre не виконуватиме перетворення таких книг. Для багатьох форматів усування DRM є доволі простою справою, але, оскільки це протизаконно, вам доведеться самостійно знайти засоби, які позбавлять вас від непотрібної залежності, а потім вже скористатися calibre для перетворення.

Здебільшого, перетворення виконується у одне клацання кнопкою. Якщо ви хочете дізнатися більше про процедуру перетворення, ознайомтеся із розділом Перетворення даних електронних книг.

У дії Перетворити книги є три варіанти, доступ до яких можна отримати за допомогою клацання кнопкою миші праворуч від відповідної кнопки.

1. Індивідуальне перетворення: надає вам змогу вказати параметри перетворення для кожної позначеної книги окремо.

2. Загальне перетворення: надає вам змогу вказати параметри для пакетного перетворення усіх позначених книг одночасно.

3. Створити каталог книг у вашій бібліотеці calibre: надає вам змогу створити повний список книг у вашій бібліотеці, включно з усіма метаданими у декількох форматах, зокрема XML, CSV, BiBTeX, EPUB і MOBI. У створеному каталозі будуть міститися усі книги, які показано на панелі бібліотеки. Це надасть вам змогу скористатися можливостями пошуку для обмеження переліку книг, які буде каталогізовано. Крім того, якщо ви позначите декілька пунктів книг за допомогою вказівника миші, до каталогу буде додано лише позначені книги. Якщо ви створите каталог у форматі електронної книги, зокрема EPUB, MOBI або AZW3, під час наступного з’єднання з вашим пристроєм для читання електронних книг каталог буде автоматично надіслано на пристрій. Щоб дізнатися більше про те, як працюють каталоги, ознайомтеся із розділом Створення каталогів AZW3 • EPUB • MOBI.

Перегляд

За допомогою кнопки Перегляд ви можете відкрити книгу у програмі для перегляду електронних книг. У calibre передбачено вбудовану програму для перегляду електронних книг у багатьох форматах. Для інших форматів буде використано типову для таких форматів програму з операційної системи. Ви можете налаштувати формати, які відкриватимуться за допомогою вбудованого засобу перегляду за допомогою пункту Налаштування  →  Інтерфейс  →  Поведінка. Якщо дані книги зберігаються у декількох форматах, ви зможете вибрати формат натисканням кнопки зі стрілочкою, розташованої праворуч від основної кнопки.

Надіслати до пристрою

У дії Надіслати до пристрою передбачено вісім варіантів, доступ до яких можна отримати за допомогою клацання кнопкою миші праворуч від відповідної кнопки.

1. Надіслати в основну пам’ять: позначені книги буде перенесено до основної пам’яті пристрою для читання електронних книг.

2. Надіслати на картку пам’яті А: позначені книги буде перенесено на картку зберігання даних A на пристрої для читання електронних книг.

3. Надіслати на картку пам’яті B: позначені книги буде перенесено на картку зберігання даних B на пристрої для читання електронних книг.

4. Надіслати певний формат на: позначені книги буде перенесено до вказаного місця на пристрої у вказаному вами форматі.

5. Від’єднати пристрій: від’єднує пристрій від calibre.

6. Встановити типову дію у відповідь на надсилання на пристрій: надає вам змогу вказати, який з варіантів, від 1 до 5 вище або 7 нижче, буде типово використано, коли ви натиснете основну кнопку.

7. Надіслати і вилучити з бібліотеки: позначені книги буде перенесено у вказане місце на пристрої для читання, а потім вилучено з бібліотеки.

8. Отримати анотації (експериментальне): перенести анотації, які ви могли створити для електронної книги на вашому пристрої, для коментування метаданих книги у бібліотеці calibre.

Ви можете керувати назвами файлів та структурою тек, які буде надіслано на пристрій налаштовуванням шаблона на сторінці Налаштування  →  Імпорт/Експорт  →  Надсилання книг на пристрої. Див. також Мова шаблонів calibre.

Отримати новини

За допомогою кнопки Отримати новини можна отримувати новини із різноманітних сайтів і перетворювати їх на електронну книгу, яку можна буде читати на пристрої для читання електронних книг. Зазвичай, новостворену книгу буде додано до вашої бібліотеки електронних книг, але якщо на час завершення отримання даних новин з комп’ютером з’єднано пристрій для читання, новини буде автоматично вивантажено на пристрій.

Засіб Отримати новини використовує прості рецепти (10-15 рядків коду) для кожного сайта новин. Щоб дізнатися більше про створення власних рецептів, ознайомтеся із розділом Додавання вашого улюбленого сайта новин.

У дії Отримати новини є три варіанти, доступ до яких можна отримати за допомогою клацання кнопкою миші праворуч від відповідної кнопки.

1. Запланувати отримання новин: надає вам змогу запланувати отримання даних із визначених вами джерел новин, які можна вибрати з сотень доступних варіантів. Планування може бути виконано окремо для кожного вибраного вами джерела новин. Ви можете вибрати певні дні тижня або інтервал у днях між сеансами отримання новин.

2. Додати власне джерело новин: надає вам змогу створити простий рецепт для отримання новин із нетипового сайта, до якого ви хочете мати доступ. Для створення рецепта достатньо вказати адресу подачі новин RSS або, якщо хочеться кращого результату, написати код мовою Python. Докладніший опис можна знайти у розділі Додавання вашого улюбленого сайта новин.

3. Отримати дані з усіх запланованих джерел новин: наказує calibre негайно розпочати отримання даних з усіх джерел новин, для яких було заплановано отримання даних.

Бібліотека

За допомогою кнопки дії Бібліотека ви зможете створювати бібліотеки, перемикатися між бібліотеками, перейменовувати і вилучати бібліотеки. calibre надає вам змогу створювати будь-яку бажану кількість бібліотек. Ви, наприклад, можете створити бібліотеку фантастики, бібліотеку художньої літератури, бібліотеку книг іноземними мовами, бібліотеку певного проекту або будь-яку іншу бібліотеку, яка відповідає вашим потребам. Бібліотеки перебувають на вищому організаційному рівні у структурі даних calibre. У кожної бібліотеки є власний набір книг, міток, категорій та власне місце для зберігання даних.

1. Перемкнути/Створити бібліотеку…: надає вам змогу: а) з’єднатися із попередньо створеною бібліотекою calibre з іншим розташуванням, б) створити порожню бібліотеку у новому місці або в) пересунути поточну бібліотеку у нове місце.

2. Швидке перемикання: надає вам змогу перемикатися між бібліотеками, які було зареєстровано або створено у межах calibre.

3. Перейменувати бібліотеку: надає вам змогу перейменувати бібліотеку.

4. Вибрати випадкову книгу з бібліотеки: програма вибере для вас випадкову книгу з бібліотеки

5. Вилучити бібліотеку: надає вам змогу скасувати реєстрацію бібліотеки у calibre.

6. Експортувати/Імпортувати усі дані calibre: надає вам змогу експортувати дані calibre для перенесення на інший комп’ютер або імпортувати раніше експортовані дані.

7. <назва бібліотеки>: дії 7, 8 тощо… нададуть вам змогу негайно перемикатися між декількома створеними вами або з’єднаними вами бібліотеками. У цьому списку міститимуться пункти для 5 найчастіше використовуваними бібліотеками. Для перемикання на інші бібліотеки використовуйте меню «Швидке перемикання».

8. Обслуговування бібліотеки: надає вам змогу наказати програмі виконати пошук проблем із відповідністю даних та відновлення поточної бази даних бібліотеки з резервної копії.

Примітка

Метадані щодо ваших книг, зокрема назв книг, авторів та міток, зберігаються у окремому файлів у теці вашої бібліотеки calibre. Цей файл має назву metadata.db. Якщо дані у цьому файлів буде пошкоджено (рідкісне явище), ви втратите ці метадані. На щастя, calibre автоматично створює резервні копії метаданих для окремих книг у теці книги у форматі файла OPF. Якщо ви скористаєтеся пунктом «Відновити базу даних» з меню «Обслуговування бібліотеки», описаного вище, ви зможете наказати calibre повторно зібрати файл metadata.db на основі даних з окремих файлів OPF.

Ви можете копіювати або пересувати книги між різними бібліотеками (якщо вами налаштовано декілька бібліотек): просто наведіть вказівник миші на пункт книги у бібліотеці, клацніть правою кнопкою миші і виберіть у контекстному меню пункт Копіювати до бібліотеки.

Пристрій

За допомогою кнопки Пристрій ви зможете переглянути книги у основній пам’яті та на картках пам’яті вашого пристрою для читання електронних книг або від’єднати пристрій (розірвати його зв’язок із calibre). Відповідну кнопку буде показано на основній панелі інструментів calibre одразу після з’єднання підтримуваного програмою пристрою з комп’ютером. Ви можете натиснути кнопку, щоб переглянути список книг на вашому пристрої. Ви також можете перетягувати пункти з книг з вашої бібліотеки calibre на піктограму і скидати їх, щоб перенести книги на ваш пристрій. І навпаки, ви можете перетягувати книги з пристрою і скидати їх на піктограму бібліотеки на панелі інструментів, щоб перенести книги з вашого пристрою до бібліотеки calibre.

Зберегти на диск

У дії Зберегти на диск передбачено п’ять варіантів, доступ до яких можна отримати натисканням стрілочки, розташованої праворуч від основної кнопки.

1. Зберегти на диск: зберігає позначені книги на диск з упорядковуванням за теками. Структура тек є такою:

Author_(sort)
Title
Book Files

Ви можете керувати назвами файлів та структурою каталогів при збереженні на диск, налаштувавши шаблон на сторінці Налаштування → Імпортування/Експортування → Запис книг на диск. Див. також розділ Мова шаблонів calibre.

2. Зберегти на диск до однієї теки: зберегти позначені книги на диску в одній теці.

Для першого і другого пунктів на диску буде збережено усі доступні формати, а також метадані книг для кожної позначеної книги. Метадані зберігатимуться у одному файлі OPF. Збережені таким чином книги можна повторно імпортувати до бібліотеки без втрати даних за допомогою кнопки Додати книги.

3. Зберегти на диск у форматі *<формат>*: зберегти позначені книги на диск у структуру тек, показану у пункті 1, але лише у вказаному форматі електронної книги. Вказати бажаний формат можна за допомогою пункту Налаштування  →  Інтерфейс  →  Поведінка  →  Бажаний формат виведення даних

4. Зберегти на диск у форматі *<формат>* до єдиної теки: зберегти позначені книги на диск до єдиної теки, але лише у бажаному форматі електронної книги. Вказати бажаний формат можна за допомогою пункту Налаштування  →  Інтерфейс  →  Поведінка  →  Бажаний формат виведення даних.

5. Зберегти на диск у єдиному форматі…: зберегти позначені книги на диск із структурою тек, показаною у першому пункті, але лише у одному форматі, який можна вибрати зі спадного списку.

З’єднатись/Роздати

За допомогою кнопки З’єднатись/Роздати ви можете вручну встановити з’єднання із пристроєм чи текою на вашому комп’ютері. Також можна налаштувати вашу бібліотеку calibre для доступу за допомогою програми для перегляду інтернету або електронної пошти.

У дії З’єднатись/Роздати є чотири варіанти, доступ до яких можна отримати за допомогою клацання кнопкою миші праворуч від відповідної кнопки.

1. З’єднатися з текою: надає вам змогу встановити з’єднання із будь-якою текою на вашому комп’ютері так, наче це додатковий пристрій, і скористатися передбаченими для пристроїв можливостями для цієї теки. Корисно, якщо підтримку вашого пристрою засобами calibre не передбачено, але пристрій доступний як диск USB.

2. Запустити сервер даних: запустити вбудований сервер даних calibre. Після запуску доступ до вашої бібліотеки calibre можна буде здійснювати за допомогою програми для перегляду інтернету (якщо ви цього захочете). Спосіб доступу до сервера можна налаштувати за допомогою сторінки Налаштування  →  Спільний доступ →  Обмін мережею

3. Налаштувати спільний доступ на основі електронної пошти: надає змогу поширювати книги і подачі новин за допомогою електронної пошти. Після налаштовування адрес електронної пошти для цієї можливості calibre надсилатиме новини та нові книги на вказані адреси електронної пошти. Налаштувати спосіб надсилання електронної пошти calibre можна за допомогою сторінки Налаштування  →  Спільний доступ  →  Обмін книгами електронною поштою. Після налаштовування одної або декількох адрес цей пункт меню буде замінено пунктами надсилання книг на налаштовані адреси.

Вилучити книжки

Використання пункту дії Вилучити книги призводить до остаточного вилучення книг, отже ним слід користуватися обачно. Пункт є залежним від контексту, тобто залежить від вибраного вами каталогу. Якщо було вибрано Бібліотеку, книги буде вилучено з бібліотеки. Якщо було вибрано пристрій для читання електронних книг, книги буде вилучено з пристрою. Щоб вилучити лише дані у певному форматі для вказаної книги, скористайтеся дією Редагувати метадані. У пункту дії вилучення книг також є п’ять варіантів, доступ до яких можна отримати натисканням стрілки, розташованої праворуч від основної кнопки.

1. Вилучити вибрані книжки: надає вам змогу остаточно вилучити усі книги, які позначено у списку книг.

2. Вилучити файли визначеного формату із вибраних книг…: надає вам змогу остаточно вилучити файли електронних книг для усіх книг, які позначено у списку книг.

3. Вилучити всі формати із вибраних книг, крім…: надає вам змогу остаточно вилучити файли електронних книг в усіх форматах, окрім вказаного, для усіх книг, які позначено у списку книг.

4. Вилучити в формати з вибраних книг: надає вам змогу остаточно вилучити усі файли електронних книг, які позначено у списку книг. У базі даних залишаться лише метадані.

5. Вилучити обкладинки з вибраних книжок: надає вам змогу остаточно вилучити файли зображень обкладинок з книг, які позначено у списку книг.

6. Вилучити відповідні книги з пристрою: надає вам змогу вилучити зі з’єднаного пристрою усі книги, які збігаються із книгами, які позначено у списку книг.

7. Відновити нещодавно вилучені: надає вам змогу скасувати вилучення книг або форматів.

Примітка

Зауважте, що коли ви користуєтеся пунктом Вилучити книги для вилучення книг з вашої бібліотеки calibre, програма вилучає запис книги, але книги тимчасово зберігаються декілька днів у теці смітника. Ви можете скасувати вилучення клацанням правою кнопкою миші на кнопці Вилучити книги із наступним вибором пункту Відновити нещодавно вилучені.

Налаштування

Дія Налаштування надає вам змогу змінити різноманітні аспекти роботи calibre. Для неї передбачено чотири варіанти, доступ до яких можна отримати натисканням стрілки, розташованої праворуч від кнопки.

1. Налаштування: надає вам змогу змінити різноманітні аспекти роботи calibre. Натискання основної кнопки призведе до виконання тієї самої дії.

2. Запустити майстер налаштувань: надає вам змогу запустити засіб початкового налаштовування програми, який ви могли бачити під час першого запуску calibre.

3. Отримати додатки для покращення calibre: відкриває вікно, у якому буде показано список додатків до calibre. Ці додатки розроблено сторонніми розробниками для розширення можливостей calibre.

4. Перезавантажити в режимі зневаджування: надає вам змогу увімкнути режим діагностики, який може допомогти розробникам calibre усунути проблеми, з якими ви зіткнулися під час роботи з програмою. Не варто вмикати цей режим просто так, якщо вас про це не попросив розробник програми.

Каталоги

Каталог — це збірка книг. calibre може керувати двома типами різних каталогів:

1. Бібліотека: це збірка книг, що зберігається у бібліотеці calibre на вашому комп’ютері.

2. Пристрій: це збірка книг, що зберігається на вашому пристрої для читання електронних книг. Цим типом каталогу можна буде скористатися, якщо ви з’єднаєте пристрій для читання електронних книг з вашим комп’ютером.

Багато дій, зокрема додавання, вилучення, перегляд книг, є залежними від контексту. Отже, наприклад, якщо ви натиснете кнопку Перегляд, коли вибрано каталог Пристрій, calibre відкриє для перегляду файли на пристрої. Якщо ж вибрано каталог Бібліотека, програма відкриє файли у вашій бібліотеці calibre.

Пошук і упорядковування

Панель пошуку і упорядковування є дуже потужним засобом роботи з вашими збірками.

- Ви можете упорядковувати збірки за назвами книг, авторами, датами, оцінками тощо. Достатньо лише натиснути відповідний заголовок стовпчика. Також можливе допоміжне упорядковування, тобто упорядковування за декількома стовпчиками даних. Наприклад, якщо ви натиснете стовпчик назви книги, а потім стовпчик авторів, книги буде спочатку упорядковано за авторами, а потім усі записи, які належать одному автору, буде упорядковано за назвою.

- Ви можете шукати певну книгу або набір книг за допомогою смужки пошуку. Докладніше про це нижче.

- Редагувати метадані просто і зручно: достатньо позначити запис, який ви хочете змінити у списку, і натиснути клавішу E.

- Ви можете виконувати Дії над наборами книг. Щоб позначити декілька книг у списку, ви можете скористатися одним з таких варіантів дій:

- Натиснути і утримувати клавішу Ctrl, одночасно наводячи вказівник миші на бажані пункти і клацаючи лівою кнопкою миші.

- Натиснути і утримувати клавішу Shift, навести вказівник на початок діапазону книг, які ви хочете позначити, клацнути лівою кнопкою миші, далі, навести вказівник на кінець діапазону і знову клацнути лівою кнопкою миші.

- Налаштувати поля, які має бути показано у списку, можна за допомогою діалогового вікна Налаштування.

- Для складного вторинного упорядковування на основі вмісту декількох стовпчиків додайте на панель інструментів кнопку інструмента Сортувати за за допомогою сторінки Налаштування → Панелі інструментів меню.

Інтерфейс пошуку

Ви можете виконувати пошук за усіма метаданими: достатньо лише ввести критерії пошуку у поле смужки пошуку. Приклад:

Asimov Foundation format:lrf

У відповідь буде показано усі книги бібліотеки, у метаданих яких є слова Asimov і Foundation і які зберігаються у форматі LRF. Ще декілька прикладів:

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

Типи пошуку

Передбачено чотири типи пошуку: містить, рівність, формальний вираз (див. формальні вирази [https://en.wikipedia.org/wiki/Regular_expression]), та варіант символів. Вибрати тип пошуку можна за допомогою символу-префікса.

Пошуки «містить»

Критерії пошуку без символу-префікса є пошуками «містить». Типово, регістр символів у них не береться до уваги. Відповідність встановлюється, якщо критерій пошуку зустрічається будь-де у вказаних метаданих. Ви можете наказати програмі враховувати регістр символів у пошуках «містить» за допомогою позначення пункту Пошук із врахуванням регістру на сторінці Налаштування / Пошук. Якщо позначено пункт Символи без акцентів відповідають символами з &акцентами, а пунктуацію слід ігнорувати, відповідність встановлюватиметься з усіма варіантами символів (наприклад, e вважатиметься відповідним для é, è, ê і ë), а усі символи пунктуації і пробіли буде проігноровано. Наприклад, якщо позначено пункт Символи без акцентів відповідають символами з акцентами, а пунктуацію слід ігнорувати і вказано такі дві назви книг:

1. Big, Bothéred, and Bad

2. Big Bummer

за цими критеріями пошуку буде знайдено:

- title:"er" — обидві книги («e» — те саме, що і «é» з «e»).

- title:"g " — обидві книги, оскільки пробіли не буде враховано.

- title:"g," — обидві книги, оскільки кому не буде враховано.

- title:"gb" — обидві книги, оскільки не буде враховано «, » у книзі 1 і пробіли у книзі 2.

- title:"g b" — обидві книги оскільки кому і пробіл не буде враховано.

- title:"db" — першу книгу, оскільки пробіл у «and Bad» не буде враховано.

- title:"," — обидві книги (насправді, усі книги), оскільки коми не буде враховано.

Якщо пункт «Символи без акцентів відповідають символами з акцентами, а пунктуацію слід ігнорувати» не буде позначено, буде взято до уваги усі варіанти символів, пунктуацію і пробіли.

Ви можете вибрати лише один варіант пошуку — «Пошук із врахуванням регістру» або «Символи без акцентів відповідають символами з &акцентами, а пунктуацію слід ігнорувати».

Пошуки «рівність»

Критерії пошуку «рівність» позначають префіксом у формі знаку рівності у рядку пошуку (=). Наприклад, запит tag:"=наукова" відповідає наукова, але не наукова фантастика або серйозна наукова. Варіанти символів також буде взято до уваги: «é» — не те саме, що «e».

Для ієрархічних записів (наприклад, A.B.C) передбачено два варіанти пошуку «рівність»: пошуки за ієрархічним префіксом і пошуки за ієрархічними компонентами. Перший варіант позначається одинарною крапкою після знаку рівності (=.) і встановлює відповідність за початковими частинами ієрархічного запису. Другий варіант позначається двома крапками після знаку рівності (=..) і встановлює відповідність внутрішній назві у ієрархічному записі. Приклади з використанням мітки History.Military.WWII як значення:

- tags:"=.History" : істина. History є префіксом мітки.

- tags:"=.History.Military" : істина. History.Military є префіксом мітки.

- tags:"=.History.Military.WWII" : істина. History.Military.WWII є префіксом мітки, хоча і неналежним.

- tags:"=.Military" : хибність. Military не є префіксом мітки.

- tags:"=.WWII" : хибність. WWII не є префіксом мітки.

- tags:"=..History" : істина. Ієрархія містить значення History.

- tags:"=..Military" : істина. Ієрархія містить значення Military.

- tags:"=..WWII" : істина. Ієрархія містить значення WWII.

- tags:"=..Military.WWII" : хибність. При пошуку .. програма шукає одинарні значення.

Пошуки за формальним виразом

Запити щодо пошуку за формальним виразом позначаються додаванням на початку рядку пошуку тильди (~). Можна використовувати будь-які сумісні з Python формальні вирази [https://docs.python.org/library/re.html]. Зауважте, що слід дублювати символи зворотної похилої риски, які використовуються для екранування символів у формальних виразах, оскільки одинарні похилі риски буде вилучено під час обробки запиту. Наприклад, якщо ви хочете знайти символ круглої дужки, вам слід вказати у виразі «\(» або скористайтеся «суперлапками» (див. нижче). Пошук за формальними виразами виконуватиметься так, щоб результати «містили» вказаний вираз, якщо у формальному виразі не визначено прив’язок до кінця або початку слова. Варіанти символів вважаються різними: ~e не є відповідником é.

Пошуки за «варіантом символів»

Пошуки за варіантом символів позначаються префіксом каре (^) у рядку пошуку. Цей варіант пошуку подібний до пошуку «містить» (див. вище), окрім таких особливостей:

- регістр символів ніколи не братиметься до уваги.

- варіанти символів завжди вважатимуться одним символом.

- пунктуація і пробіли завжди братимуться до уваги.

Пункти коригування пошуків Символи без акцентів відповідають символами з акцентами, а пунктуацію слід ігнорувати і Пошук із врахуванням регістру буде проігноровано. Вони не впливатимуть на роботу цього варіанта пошуків.

Нижче наведено порівняння цього типу пошуків із пошуком «містить», якщо позначено пункт Символи без акцентів відповідають символами з акцентами, а пунктуацію слід ігнорувати (див. вище) і використано ті самі дві назви книг:

1. Big, Bothéred, and Bad

2. Big Bummer

тоді такі пошуки із варіантами символів призведуть до вказаних результатів:

- title:"^er" — обидві книги («e» — те саме, що і «é» з «e»)

- title:"^g" — обидві книги

- title:"^g " — книга 2, оскільки пробіли мають значення

- title:"^g," — книга 1, оскільки кома має значення

- title:"^gb" — нічого, оскільки пробіл і кома мають значення

- title:"^g b" — книга 2, оскільки кома має значення

- title:"^db" — нічого

- title:"^," — книга (а не усі книги), оскільки кома має значення

Синтаксис виразу для пошуку

Вираз для пошуку є послідовністю термінів для пошуку, які може бути відокремлено операторами and та or. Якщо два терміни для пошуку вказано без оператора-роздільника, буде використано оператор and. Оператор and має вищий пріоритет за оператор or; наприклад, вираз a or b and c є тим самим, що a or (b and c). Ви можете скористатися дужками для зміни пріоритетності; наприклад, (a or b) and c, щоб or було використано до and. Ви можете скористатися оператором not для заперечення (інверсії) результату обчислення виразу для пошуку. Приклади:

- not tag:щось знайде усі книги, у записах яких не міститься мітки щось

- not (author:Азімов or author:Вебер) шукає усі книги, які не написано ні Азімовим, ні Вебером.

Вище наведено приклади термінів для пошуку. Базовим терміном для пошуку є послідовність символів, у якій немає пробілів, символів лапок ("), символів зворотної похилої риски (\) або дужок (( )). Перед ним може бути вказано специфікатор назви стовпчика: після назви пошуку стовпчика має бути вказано двокрапку (:), наприклад, author:Азімов. Якщо у терміні для пошуку має бути пробіл, увесь термін для пошуку слід взяти у лапки, наприклад, title:"The Ring". Якщо у терміні для пошуку мають бути лапки, їх слід екранувати символами зворотної похилої риски. Наприклад, щоб знайти цикл із назвою The «Ball» and The «Chain», скористайтеся таким виразом:

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

Якщо вам потрібен сам символ зворотної похилої риски, як це часто трапляється у пошуках із формальними виразами, скористайтеся подвоєнням (\\).

Іноді виконати правильне екранування для отримання бажаного результату досить важко, особливо у пошуках із формальними виразами і шаблонами. У таких випадках скористайтеся суперлапками: """послідовність символів""". Символи у суперлапках використовуються без змін: їхня обробка не відбуватиметься.

Додаткові відомості

Щоб виконати пошук рядка, який починається із символу рівності, тильди або каре; додайте до рядка префікс у формі символу зворотної похилої риски.

Якщо рядок пошуку містить дужки або пробіли, його слід взяти у лапки («). Наприклад, щоб знайти записи з міткою «наукова фантастика», вам слід ввести «tag:»=science fiction»». Якщо ви введете критерій пошуку «tag:=science fiction», буде знайдено усі книги з міткою «наукова» і словом «фантастика» у метаданих.

Ви можете будувати складні запити щодо пошуку за допомогою діалогового вікна розширеного пошуку, доступ до якого можна отримати натисканням кнопки ліворуч від поля пошуку.

Доступними для пошуку є такі поля: tag, title, author, publisher, series, series_index, rating, cover, comments, format, identifiers, date, pubdate, search, size, vl, а також нетипові стовпчики. Якщо з комп’ютером з’єднано пристрій для читання електронних книг, буде доступним поле ondevice, якщо пошук відбувається на панелі бібліотеки calibre. Для визначення назви поля пошуку для нетипового стовпчика, наведіть вказівник миші на заголовок стовпчика і почекайте, доки програма покаже контекстну панель із назвою.

Дати

Синтаксис запиту щодо пошуку за датою:

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

Якщо дата є неоднозначною, для порівняння дат використовуватимуться поточні дані локалі. Наприклад, у локалі з датою у форматі mm/dd/yyyy дата 2/1/2009 відповідає 1 лютого 2009 року. У локалі dd/mm/yyyy той самий запис відповідатиме 2 січня 2009 року. Передбачено також декілька спеціальних рядків для дат. Рядок today відповідає поточній даті, якою б вона не була. Також можна скористатися рядками yesterday і thismonth (або перекладеними еквівалентами відповідною мовою, «вчора» і «цьогомісяця», відповідно). Крім того, можна скористатися рядком daysago («днівтому») для порівняння з поточною датою. Приклад:

date:>10daysago
date:<=45daysago

Щоб уникнути потенційних проблем із перекладами рядків, якщо використовується неанглійська версія calibre, рядки _today, _yesterday, _thismonth, and _daysago доступні у будь-якому перекладі. Вони просто не перекладаються.

Пошук дат і числових значень з умовами порівняння

У полях дат і числових полях можна використовувати оператори порівняння = (дорівнює), > (більше), >= (більше або дорівнює), < (менше), <= (менше або дорівнює) та != (не дорівнює). Поля оцінок вважаються числовими. Наприклад, якщо вказано рядок пошуку rating:>=3, буде показано усі книги з оцінкою 3 і більше.

Ви можете шукати книги, які мають певний розмір у якомусь із форматів, ось так:

- size:>1.1M — буде знайдено книги, файл у певному форматі яких має розмір, що перевищує 1,1 МБ

- size:<=1K — буде знайдено книги, які у якомусь форматі мають розмір, який менший або рівний 1 кБ

Можна шукати за кількістю записів у багатозначних полях, зокрема міток. Запити щодо таких пошуків мають починатися з символу #. Далі слід використовувати ті самі синтаксичні конструкції, що і у числових полях. Наприклад, щоб знайти усі книги, у яких понад 4 мітки, скористайтеся запитом tags:#>4. Щоб знайти усі книги, у яких точно 10 міток, введіть запит tags:#=10.

Номери у циклі

Можна виконувати пошук за індексом у циклу. Для стандартних серій книг слід використовувати назву «series_index». Для нетипових стовпчиків циклу використовуйте назву стовпчика із додаванням рядка _index. Наприклад, щоб знайти книгу за індексом у нетиповому стовпчику циклу із назвою #my_series, слід скористатися назвою для пошуку #my_series_index. Індекси у циклі є числами, тому для них можна використовувати оператори відношень, які описано вище.

Збережені результати пошуків

Спеціальне поле search використовується для збережених пошуків. Отже, якщо ви зберегли пошук із назвою «Книги моєї дружини», ви можете ввести search:"Книги моєї дружини" у поле для пошуку і скористатися вже отриманими результатами. Докладніше про це у розділі щодо збереження результатів пошуку, наведеному нижче.

Віртуальні бібліотеки

Спеціальне поле vl використовується для пошуку у віртуальній бібліотеці. Наприклад, за критерієм «vl:Прочитане» буде знайдено усі книги у віртуальній бібліотеці «Прочитане». За критерієм пошуку «vl:Прочитане and vl:»Наукова фантастика»» буде знайдено усі книги, які належать одночасно до віртуальних бібліотек «Прочитане» і «Наукова фантастика». Значення після «vl:» має бути назвою віртуальної бібліотеки. Якщо у назві віртуальної бібліотеки є пробіли, слід взяти її у лапки.

Чи має поле значення

Ви можете шукати книги, які не містять або містять у певному полі значення, за допомогою «true» і «false». Приклад:

- cover:false — знайде усі книги без зображення обкладинки

- series:true — знайде усі книги, які є частинами циклів

- series:false — знайде усі книги, які не є частинами циклів

- comments:false — знайде усі книги, які мають порожній коментар

- formats:false — знайде усі книги, які не мають файлів (порожні записи)

Нетипові стовпчики «Так/Ні»

Дані можна шукати і у нетипових стовпчиках із булевими значеннями («так» або «ні»). Якщо ви шукатимете false або порожньо, буде знайдено усі книги із невизначеними значеннями у стовпчику. Якщо шукати true, буде знайдено усі книги, у яких значення у стовпчику визначено. Якщо шукати так або позначено, буде знайдено усі книги із Так у відповідному стовпчику. Пошук ні або не_позначено надасть змогу знайти усі книги з Ні у стовпчику. Зауважте, що слова yes, no, blank, empty, checked та unchecked може бути перекладено. Ви можете скористатися або перекладом цих слів, або англійським еквівалентом. Слова true і false, а також особливі значення _yes, _no та _empty не перекладаються.

Ідентифікатори

Для ідентифікаторів (наприклад ISBN, DOI, LCCN тощо) також використовують розширений синтаксис. По-перше, зауважте, що ідентифікатор має форму тип:значення, наприклад isbn:123456789. Розширений синтаксис надає вам змогу вказати незалежно тип і значення, які ви шукаєте. Як для типу, так і для значення у запиті можна використовувати будь-які типи пошуку. Приклади:

- identifiers:true — знайти крити з будь-яким ідентифікатором.

- identifiers:false — знайти книги без ідентифікатора.

- identifiers:123 — знайти книги з ідентифікатором будь-якого типу, який має значення, що містить рядок «123».

- identifiers:=123456789 — знайти книги з ідентифікатором будь-якого типу рівним «123456789».

- identifiers:=isbn: і identifiers:isbn:true — знайти книги, у яких тип ISBN має будь-яке значення.

- identifiers:=isbn:false — знайти усіх книги без типу рівного ISBN.

- identifiers:=isbn:123 — знайти книги, у яких тип ISBN має значення, що містить рядок «123».

- identifiers:=isbn:=123456789 — знайти книги, у яких тип ISBN має значення рівне «123456789».

- identifiers:i:1 — знайти книги, у яких тип, назва якого містить літеру «i», має значення, яке містить символ «1».

Категорії, які є видимими на панелі навігації мітками

Пошук in_tag_browser:true призведе до виявлення усіх книг, які належать до категорій (міток, авторів тощо), які показано на панелі Навігатор мітками. Це корисно, якщо вами налаштовано дві речі: Налаштування → Вигляд та поведінка → Навігатор мітками → Приховати порожні категорі та Пошука показує усі відповідні записи. Якщо налаштовано дві ці речі натискання пункту Знайти на панелі Навігатор мітками покаже лише категорії, що містять записи, що відповідають пошуку Знайти. Далі, пошук in_tag_browser:true додатково знайде книги у цих категоріях / записах. Ви можете без проблем запускати цей пошук натисканням комбінації клавіш Ctrl+Alt+Shift+F або кнопки налаштовування на панелі Навігатор мітками і вибором пункту Показати лише книги, які належать до видимих категорій.

Пошук за допомогою шаблонів

Ви можете шукати за допомогою шаблона у Мова шаблонів calibre, замість поля метаданих. Для цього слід вказати шаблон, тип пошуку та значення, яке слід знайти. Синтаксис:

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

Шаблоном є будь-який коректний шаблон мови шаблонів calibre. Типом пошуку має бути t (текстовий пошук), d (пошук за датою), n (числовий пошук) або b (пошук «встановлено/не встановлено» (булевий)). Значенням може бути будь-що. Для створення різних :ref:`типів пошуку <search_kinds>“ можна використовувати спеціальні оператори, які описано вище. Якщо у рядку пошуку містяться пробіли, його слід взяти у лапки.

Приклади:

- template:"program: connected_device_name('main')#@#:t:kindle" приймає значення «true», якщо з комп’ютером з’єднано пристрій kindle

- template:"program: select(formats_sizes(), 'EPUB')#@#:n:>1000000" — знайти книги у файлах EPUB, розмір яких перевищує 1 МБ.

- template:"program: select(formats_modtimes('iso'), 'EPUB')#@#:d:>10daysago" — знайти книги у файлах EPUB, часова позначка яких є не старішою за 10 днів від поточної дати

- template:"""program: book_count('tags:^"' & $series & '"', 0) != 0#@#:n:1""" — знайти усі книги, що містять назву циклу у мітках. У цьому прикладі використано суперлапки, оскільки у шаблоні використано одразу одинарні лапки (') і подвійні лапки (") при побудові виразу для пошуку.

Будувати шаблони пошуку можна за допомогою Діалогового вікна розширеного пошуку, відкрити яке можна за допомогою натискання кнопки . Перевірити шаблон на певних книгах можна за допомогою Засобу тестування шаблонів`calibre. Відповідний пункт можна додати на панель інструментів або до меню за допомогою сторінки :guilabel:`Налаштування → Панелі і меню. Також можна пов’язати із цією дією клавіатурне скорочення за допомогою сторінки Налаштування → Скорочення.

Вікно складного пошуку

Діалогове вікно розширеного пошуку

Збереження результатів пошуку
calibre надає вам змогу зберігати часто використовувати пошуки під спеціальними назвами, а потім використовувати їх за допомогою одного клацання кнопкою миші. Створити свій пошук можна введенням критерію пошуку на панелі пошуку або вибором відповідного пункту на панелі перегляду міток. Далі, вам слід ввести назву, яку ви хочете дати пошуку, у поле збережених пошуків, розташоване поряд із смужкою пошуку. Потім слід натиснути кнопку з піктограмою «плюс», розташовану поряд із полем для назви збереженого пошуку, щоб зберегти пошук із вказаною назвою.

Після цього, доступ до збереженого пошуку можна буде здійснювати за допомогою панелі перегляду міток, а саме розділу Збережені пошуки. Будь-яким, як завгодно складним пошуком, можна буде скористатися дуже просто: нічого не треба буде створювати, достатньо буде один раз клацнути кнопкою миші.

Повнотекстовий пошук в усіх книгах
calibre може, якщо ви захочете, індексувати текст усіх книг у бібліотеці для уможливлення миттєвого пошуку слів у будь-якій книзі. Щоб увімкнути цю функціональну можливість, натисніть кнопку FT на лівому краю панелі пошуку.

Далі, увімкніть індексування для поточної бібліотеки. Щойно індексування буде завершено, ви зможете виконувати повнотекстовий пошук в усій бібліотеці. Після додавання до бібліотеки книг програма буде автоматично індексувати їхній вміст у фоновому режимі. За допомогою цього режиму пошуку ви зможете швидко знайти книгу, яка містить певне слово або певну комбінацію слів. Ви навіть зможете шукати слова, які розташовано поруч із іншими словами, як це показано у прикладах у контекстному вікні пошуку. Зауважте, що цей інструмент пошуку знайде лише одне входження пошукового критерію у певній книзі, а не покаже список усіх входжень. Щоб знайти щось у певній книзі, варто скористатися пошуком у Засобі перегляду книг calibre.

Ви можете наказати програмі виконати повторне індексування окремої книги, клацнувши правою кнопкою на панелі подробиць щодо книги у вікні calibre і вибравши Повторно індексувати цю книгу для повнотекстового пошуку.

Віртуальні бібліотеки

Віртуальна бібліотека — це спосіб «обманути» calibre, повідомивши програмі, що у вашій бібліотеці усього декілька книг, а не уся збірка. Це чудовий спосіб поділити вашу велику збірку книжок на менші, зручніші для роботи фрагменти. Щоб дізнатися більше про те, як створювати віртуальні бібліотеки, і як ними користуватися, ознайомтеся із відповідними настановами: Віртуальні бібліотеки.

Тимчасове позначення книг

Ви можете тимчасово позначати довільні набори книг. Для позначених книг буде показано шпильку. Знайти їх можна буде за критерієм marked:true. Щоб позначити книгу, натисніть комбінацію клавіш Ctrl+M або скористайтеся сторінкою Налаштування → Панелі інструментів і меню і додайте кнопку Позначити книги на головну панель інструментів.

Ви можете позначити книги певною текстовою міткою: клацніть правою кнопкою миші на кнопці Позначити книги і виберіть варіант Позначити книги текстовою міткою. Знайти книги, які позначено текстовою міткою, можна за допомогою критерію marked:"=введений-вами-текст".

Визначення метаданих з назв файлів

Зазвичай, calibre читає метадані з самого файла книги. Втім, програму можна налаштувати так, щоб вона читала метадані з назви файла. Для цього скористайтеся пунктом Налаштування  →  Імпортування/Експортування  →  Додавання книг  →  Читати метадані з даних файлів.

Ви також можете керувати тим, як метадані читатимуться з назви файла, за допомогою формальних виразів (див. розділ Все про використання формальних виразів в calibre). У розділі Додавання книг діалогового вікна налаштовування програми ви можете вказати формальний вираз, яким користуватиметься calibre для виявлення і обробки метаданих у назвах файлів електронних книг, які ви додаєте до бібліотеки. Типовим формальним виразом є такий:

title - author

тобто, програма припускає, що усі символи до першого символу - є назвою книги, а усі наступні символи — ім’я її автора. Наприклад, якщо файл має таку назву:

Foundation and Earth - Isaac Asimov.txt

програма вважатиме, що книга називається «Foundation and Earth», а її автором є Isaac Asimov

Порада

Якщо у назві файла не міститься дефіса, вказаний вище формальний вираз не спрацює.

Подробиці щодо книги

На панелі подробиць щодо книги буде показано зображення обкладинки та усі метадані поточної позначеної книги. Панель можна приховати натисканням кнопки, розташованої у нижньому правому куті основного вікна calibre. Пункт імен авторів, який показано на панелі подробиць щодо книги, можна натискати. У відповідь буде типово відкрито сторінку Вікіпедії, присвячену авторові. Дію у відповідь на натискання можна змінити. Для цього достатньо навести вказівник на пункт автора, клацнути правою кнопкою миші і вибрати у контекстному меню пункт «Керувати».

Так само, якщо ви отримували метадані щодо книги, на панелі подробиць щодо книги буде автоматично показано посилання на сторінку книги у системах Amazon, WorldCat тощо, звідки було отримано метадані.

Ви можете навести вказівник на запис окремого формату електронної книги на панелі подробиць щодо книги, клацнути правою кнопкою миші і вибрати у контекстному меню пункт вилучення, порівняння з початковими версіями, збереження на диск, відкриття у зовнішніх програмах тощо.

Ви можете змінити зображення обкладинки книги простим перетягуванням нового зображення і скиданням його на зображення, показане на панелі подробиць щодо книги. Якщо вам потрібно виконати редагування зображення обкладинки у зовнішній програмі, просто наведіть на нього вказівник миші, клацніть правою кнопкою миші і виберіть у контекстному меню пункт Відкрити за допомогою.

Ви також можете додати файли електронних книг до поточного запису книги простим перетягуванням їх на панель подробиць щодо книги з наступним скиданням.

Подвійне клацання на панелі подробиць щодо книги відкриє окреме контекстне вікно подробиць.

Нарешті, ви можете налаштувати вміст панелі подробиць щодо книги за допомогою сторінки Налаштування  →  Інтерфейс  →  Зовнішній вигляд  →  Подробиці щодо книги.

Переглядач міток

За допомогою панелі перегляду міток ви можете без проблем здійснювати навігацію вашої збірки за автором, мітками, циклами тощо. Якщо ви натиснете будь-який з пунктів на панелі перегляду міток, наприклад ім’я автора «Isaac Asimov», у списку книг праворуч буде показано лише книги цього автора. Також можна натискати пункти назв категорій. Наприклад, якщо ви натиснете пункт категорії «Цикл», у списку залишаться лише книги, які належать до циклів.

Перше клацання на пункті залишить у списку книг лише ті, які містять пункт або відповідають пунктові. Продовжуючи попередній приклад, натискання пункту «Isaac Asimov» призведе до показу книг, автором яких є Айзек Азімов. Повторне натискання пункту змінить список залежно від того, чи є у пункту дочірні записи (див. підкатегорії та ієрархічні записи нижче). У нашому прикладі з Айзеком Азімовим, повторне натискання пункту «Isaac Asimov» залишить у списку лише книги, автором яких не є Айзек Азімов. Після третього натискання пункту обмеження буде знято, і програма покаже у списку усі книги. Якщо ви натиснете і утримуватимете натиснутою клавішу Ctrl або Shift, а потім натиснете декілька пунктів, буде створено обмеження на основі позначених таким чином пунктів. Наприклад, ви можете натиснути Ctrl, а потім натиснути пункти «Історія» і «Європа», щоб знайти усі книги з історії Європи. Панеллю перегляду міток можна скористатися для побудови форматних виразів, які автоматично вводитимуться до рядка пошуку. Спостереження за виразами у рядку пошуку може допомогти вам вивчити мову побудови базових виразів для пошуку у збірці.

Програма розфарбовує піктограми пунктів на панелі перегляду міток. Рівень розфарбовування залежить від середньої оцінки книг у відповідній категорії. Отже, наприклад, якщо книги Айзека Азімова мають середню оцінку у чотири зірки, піктограму пункту «Isaac Asimov» на панелі перегляду міток буде розфарбовано на 4/5. Ви можете навести вказівник на піктограму пункту і затримати його там, щоб побачити середню оцінку.

Пункти зовнішнього рівня на панелі перегляду міток, зокрема пункти «Автори» та «Цикл», називаються категоріями. Ви можете створювати власні категорії, які називаються категоріями користувача, для упорядковування записів. Наприклад, ви можете скористатися редактором категорій користувача (натисніть кнопку Налаштувати у нижній лівій частині панелі Навігація мітками і виберіть Керування записами авторів, мітками тощо  →  Категорії користувача) для створення категорії «Улюблені автори», а потім заповнити цю категорію пунктами ваших улюблених книг. У категорій користувача можуть бути підкатегорії. Наприклад, категорія користувача «Улюблені.Автори» є підкатегорією категорії «Улюблені». Також може бути створено підкатегорію «Улюблені.Цикли», таким чином утворивши дві підкатегорії у категорії «Улюблені». Підкатегорії можна створити клацанням правою кнопкою миші на пункті категорії користувача з наступним вибором у контекстному меню пункту :guilabel::Додати підкатегорію до… і введенням назви підкатегорії. Також можна скористатися редактором категорій користувача, вводячи назви підкатегорій у форматі, який ми використали у наведеному вище прикладі для категорії «Улюблені».

Ви можете шукати за визначеними вами категоріями у той самий спосіб, що і за вбудованими категоріями. Достатньо просто натиснути у списку відповідний пункт. Передбачено чотири різних варіанти пошуку, якими можна циклічно переходити клацанням:

1. «усе, що відповідає пункту у категорії», позначається одним зеленим значком «плюс».

2. «усе, що відповідає пункту у категорії і усіх її підкатегоріях», позначається двома зеленими значками «плюс».

3. «усе, що не відповідає пункту у категорії», позначається одним червоним значком «мінус».

4. «усе, що не відповідає пункту у категорії і усіх її підкатегоріях», позначається двома червоними значками «мінус».

Також можна створювати ієрархії у деяких з текстових категорій, зокрема у категоріях міток, циклів та нетипових стовпчиків. До пунктів таких ієрархій додаватиметься невеличке зображення трикутничка, яке можна буде натиснути для згортання або розгортання списку пунктів у категорії. Щоб скористатися ієрархіями пунктів у категоріях, вам слід спочатку відкрити сторінку Налаштування  →  Інтерфейс  →  Зовнішній вигляд і ввести назви категорій до списку «Категорії з ієрархічними пунктами». Після цього, пункти у відповідній категорії, у назвах яких міститимуться крапки, буде показано із зображенням невеличкого трикутничка. Наприклад, припустімо, що ви створили нетиповий стовпчик із назвою «Жанр» і вказали, що у ньому міститимуться ієрархічні пункти. Після цього пункти «Загадки.Трилер» і «Загадки.Англійська» буде показано у форматі списку підпунктів пункту «Загадки» із невеличким зображенням трикутничка. Натискання трикутничка призведе до показу підпунктів «Трилер» і «Англійська». Щоб дізнатися більше ознайомтеся з розділом Керування підгрупами книг на прикладі підгрупи «жанр».

Для ієрархічних пунктів (пунктів з дочірніми елементами) використовуються такі самі чотири режими пошуку, що і для визначених вами категорій. Для пунктів, у яких немає дочірніх пунктів, використовується два режими пошуку: «усе, що відповідає» і «усе, що не відповідає».

Для додавання пунктів до категорій достатньо перетягнути відповідний пункт на категорію користувача на панелі перегляду міток. Якщо джерелом є категорія користувача, утримування натиснутою клавіші Shift під час перетягування призведе до пересування пункту до нової категорії. Ви також можете перетягувати і скидати пункти книг зі списку книг на пункти на панелі перегляду міток. Скидання пункту книги на пункт категорії означатиме, що книгу буде автоматично приписано до відповідної категорії. Наприклад, перетягування книги на пункт «Isaac Asimov» встановить для властивості автора книги значення «Isaac Asimov». Скидання пункту книги на мітку «Історія», додасть до міток книги мітку «Історія».

Знайти щось на панелі перегляду міток дуже просто: натисніть кнопку пошуку у правому нижньому куті панелі. Крім того, ви можете навести вказівник миші на будь-який з пунктів у списку, клацнути правою кнопкою миші і отримати доступ до декількох типових дій. Прикладами таких дій є приховування пункту, перейменування пункту або відкриття діалогового вікна «Керування x», за допомогою якого ви зможете керувати пунктами відповідного типу. Наприклад, за допомогою діалогового вікна «Керування пунктами «Автори»» можна перейменовувати записи авторів та керувати упорядковуванням імен авторів у списку.

Керувати способом упорядковування записів у списку панелі перегляду міток можна за допомогою кнопки Налаштувати, розташованої у нижній лівій частині панелі навігації мітками. Ви можете вибрати упорядкування за назвою, середньою оцінкою або популярністю (популярність — це кількість книг із відповідною міткою у вашій бібліотеці; наприклад, популярністю мітки «Isaac Asimov» є кількість книг у вашій бібліотеці, автором яких є Айзек Азімов).

Таблиця обкладинок

Ви можете наказати calibre показати таблицю обкладинок книг замість списку книг, якщо надаєте перевагу перегляду обкладинок книг вашої збірки. Режим Таблиця обкладинок можна задіяти натисканням кнопки Компонування у нижньому правому куті основного вікна calibre. Налаштувати розміри зображень обкладинок та тло таблиці можна за допомогою сторінки Налаштування  →  Інтерфейс  →  Зовнішній вигляд  →  Таблиця обкладинок. Ви можете навіть налаштувати усе так, щоб під обкладинкою було показано вміст певного поля, зокрема назви книги, імен авторів, оцінки або типового поля.

Переглядач обкладинок

Окрім таблиці обкладинок, описаної вище, ви можете наказати calibre показати зображення обкладинок у один рядок. Задіяти такий показ можна за допомогою кнопки Компонування, розташованої у правому нижньому куті основного вікна програми. За допомогою сторінки Налаштування  →  Інтерфейс  →  Зовнішній вигляд ви можете змінити кількість показаних обкладинок або наказати переглядачеві обкладинок показувати дані у окремому контекстному вікні.

Додавання нотаток щодо авторів, циклів тощо
ви можете додавати нотатки для записів авторів, циклів, міток, видавців тощо у вашій бібліотеці calibre. Щоб зробити це належним чином, клацніть правою кнопкою миші на імені автора на панелі Навігація мітками ліворуч або на панелі Подробиці щодо книги праворуч і виберіть пункт Створити нотатку або Редагувати нотатку.

Просте контекстне вікно, за допомогою якого ви зможете вводити свої нотатки з використанням базового форматування та підтримкою посилань та зображень. Щойно буде створено нотатку щодо автора, її можна буде без проблем переглянути на панелі Подробиці щодо книги після клацання на маленькій піктограмі олівця поряд із іменем автора.

Ви можете здійснювати пошук в усіх нотатках у вашій бібліотеці за допомогою інструмента Навігація нотатками: натисніть комбінацію клавіш Ctrl+Shift+N або додайте відповідну кнопку на панель інструментів за допомогою сторінки Налаштування → Панелі і меню.

Швидкий перегляд

Іноді виникає потреба у швидкому отриманні списку книг із тим самим значенням у тій самій категорії (автори, мітки, видавець, цикл тощо), що і поточна книга, але без зміни поточного режиму перегляду бібліотеки. Зробити це можна за допомогою швидкого перегляду. Засіб швидкого перегляду відкриває друге вікно програми або панель у списку книг, де показує список книг, які відповідають певним характеристикам. Нехай, наприклад, ви хочете переглянути список усіх книг, авторами яких є один або декілька авторів позначеної книги. Клацніть на комірці потрібного вам автора і натисніть клавішу Q або піктограму Швидкий перегляд у розділі Компонування вікна calibre. У відповідь буде відкрито вікно або панель із усіма авторами книги ліворуч і усіма книгами позначеного автора праворуч.

Декілька прикладів використання швидкого перегляду. Швидко знайти інші книги, які

- мають певні мітки, які застосовуються до поточної позначеної книги,

- належать до тієї самого циклу, що і поточна позначена книга,

- мають ті самі значення у нетиповому стовпчику, що і поточна книга,

- написано одним із авторів поточної книги,

- мають спільні значення у нетиповому стовпчику

Існує два варіанти показу даних швидкого перегляду:

1. Може бути відкрито «окрема» вікно, вікно над вікном calibre, яке лишатиметься відкритим, доки ви явним чином його не закриєте.

2. Може бути відкрито панель у розділі списку книг головного вікна calibre.

Ви можете перемикати режим роботи засобу за допомого кнопки «Пришвартувати/Відшвартувати».

Панель швидкого перегляду можна не закривати, вона автоматично стежитиме за змінами у списку книг. Наприклад, якщо на панелі бібліотеки calibre ви натиснете стовпчик категорії (мітки, циклу, видавця, автора тощо) певної книги, вміст вікна швидкого перегляду буде автоматично змінено так, щоб на панелі ліворуч було показано записи у відповідній категорії для поточної книги (наприклад мітки цієї книги). У списку буде позначено перший пункт, а на правій панелі вікна швидкого перегляду буде показано список усіх книг у вашій бібліотеці, де використовується це значення. Якщо ви натиснете якийсь інший пункт на лівій панелі вікна швидкого перегляду, список на правій панелі буде змінено відповідним чином.

Подвійне клацання на пункті книги у вікні швидкого перегляду призведе до позначення пункту книги на панелі перегляду бібліотеки. Також це призведе до зміни показу пунктів у вікні швидкого перегляду (на лівій панелі) так, щоб у вікні було показано дані для щойно позначеної книги.

Подвійне клацання на пункті книги у вікні швидкого перегляду з одночасним натисканням клавіші Shift (або Ctrl) призведе до відкриття діалогового вікна редагування метаданих відповідної книги у calibre. Для редагованої книги буде відкрито панель швидкого перегляду, коли ви закриєте діалогове вікно редагування метаданих.

Дізнатися про те, чи можна використовувати стовпчик для швидкого перегляду, можна навівши вказівник миші на заголовок стовпчика. Дані буде показано на панелі контекстної підказки. Також можна пошукати пункт «Швидкий перегляд» у контекстному меню заголовка стовпчика. Вибір цього пункту еквівалентний до натискання клавіші «Q» після позначення відповідної комірки.

Параметри (на сторінці Налаштування → Вигляд і поведінка → Швидкий перегляд):

- Брати (чи не брати) до уваги поточну віртуальну бібліотеку. Якщо позначено, на панелі швидкого перегляду буде показано лише книги із поточної віртуальної бібліотеки. Типове значення: брати до уваги віртуальні бібліотеки

- Змінювати вміст вікна швидкого перегляду, якщо у списку книг за допомогою клавіш зі стрілками змінено стовпчик. Типове значення: не змінювати вміст у відповідь на натискання клавіш зі стрілками

- Змінювати стовпчик швидкого перегляду у відповідь на подвійне клацання на комірці у вікні швидкого перегляду. Якщо параметр не задіяно, книга змінюватиметься, але стовпчик лишатиметься тим самим. Типове значення: змінювати стовпчик

- Змінювати стовпчик швидкого перегляду на поточний стовпчик, якщо на панелі швидкого перегляду натиснуто клавішу Enter. Якщо параметр не задіяно, книга змінюватиметься, але стовпчик лишатиметься тим самим. Типове значення: змінювати стовпчик

- Вибрати, які стовпчики буде показано у вікні або на панелі швидкого перегляду.

Завдання

На панелі завдань буде показано дані щодо кількості поточних запущених завдань. Завдання — це задачі, які програма виконує у межах окремого процесу. До завдань належать перетворення книг та обмін даними з вашим пристроєм для читання електронних книг. Для отримання доступу до списку завдань достатньо клацнути лівою кнопкою миші на панелі завдань. Щойно виконання завдання буде завершено, ви зможете переглянути докладний звіт щодо виконання у відповідь на подвійне клацання лівою кнопкою миші на пункті у списку завдань. Це корисно для діагностики проблем у виконанні завдань.

Клавіатурні скорочення

У calibre передбачено декілька клавіатурних скорочень, які допоможуть вам пришвидшити роботу з програмою у уникнути рухів рукою у напрямку миші. Ці клавіатурні скорочення діють на панелі списку книг (якщо ви не редагуєте властивості певної книги). Більшість з них стосуються позначеного запису книги. У засобі перегляду книг calibre передбачено власний набір клавіатурних скорочень, який можна налаштувати натиснувши кнопку Налаштування у вікні засобу перегляду електронних книг.

Примітка

Зауваження: у клавіатурних скороченнях calibre не використовуються клавіші-модифікатори (Command, Option, Control тощо), якщо це не вказано додатково. Вам достатньо натиснути клавішу із літерою, наприклад «E» для редагування.

Клавіатурні скорочення основної програми calibre

Клавіатурні скорочення

Дія

F2 (Enter у macOS)

Редагувати метадані поточного позначеного поля у списку книг.

A

Додати книги

Shift+A

Додати формати позначених книг

C

Перетворити позначені книги

D

Надіслати до пристрою

Del

Вилучити вибрані книжки

E

Редагувати метадані позначених книг

G

Отримати книги

I

Показати подробиці щодо книги

K

Редагування змісту

M

Об’єднати позначені записи

Alt+M

Об’єднати позначені записи із збереженням оригіналів

O

Відкрити теку, яка містить дані

P

Покращення книг

S

Зберегти на диск

T

Редагувати книгу

V

Перегляд

Shift+V

Переглянути останню прочитану книгу

Alt+V/Cmd+V у macOS

Переглянути в вибраному форматі

Alt+Shift+J

Увімкнути або вимкнути показ списку завдань

Alt+Shift+B

Увімкнути або вимкнути показ панелі навігації обкладинками

Alt+Shift+D

Увімкнути або вимкнути показ панелі параметрів книги

Alt+Shift+T

Увімкнути або вимкнути показ панелі навігації мітками

Alt+Shift+G

Увімкнути або вимкнути показ таблиці обкладинок

Alt+A

Показати книги, автором яких є автор поточної книги

Alt+T

Показати книги із тими самими мітками, що і поточна книга

Alt+P

Показати книги того самого видавця, що і поточна книга

Alt+Shift+S

Показати книги з того самого циклу, що і поточна книга

/, Ctrl+F

Перевести фокус на панель пошуку

Shift+Ctrl+F

Відкрити діалогове вікно складного пошуку

Shift+Alt+F

Увімкнути або вимкнути панель пошуку

Esc

Спорожнити поточний пошук

Shift+Esc

Фокусувати список книг

Ctrl+Esc

Спорожнити віртуальну бібліотеку

Alt+Esc

Вилучити додаткове обмеження

Ctrl+*

Створити тимчасову віртуальну бібліотеку на основі поточних результатів пошуку

Ctrl+→

Вибрати вкладку наступної віртуальної бібліотеки

Ctrl+←

Вибрати вкладку попередньої віртуальної бібліотеки

N або F3

Знайти наступну книгу, яка відповідає поточним критеріям пошуку (працюватиме, лише якщо у параметрах пошуку увімкнено підсвічування результатів)

Shift+N або Shift+F3

Знайти попередню книгу, яка відповідає поточним критеріям пошуку (працюватиме, лише якщо у параметрах пошуку увімкнено підсвічування результатів)

Ctrl+D

Отримати метадані та зображення обкладинок

Ctrl+R

Перезапустити Сalibre

Ctrl+Shift+R

Перезапустити calibre у діагностичному режимі

Shift+Ctrl+E

Додати порожні книги до збірки calibre

Ctrl+M

Перемкнути стан позначено/не позначено для позначених книг

Ctrl+/ або Ctrl+Alt+F

Відкрити контексту панель пошуку для повнотекстового пошуку в усіх книгах бібліотеки

Q

Відкрити контекстну панель швидкого перегляду для перегляду книг у пов’язаних циклах, мітках тощо.

Shift+Q

Перевести фокус на відкриту панель швидкого перегляду

Shift+S

Виконати пошук на панелі швидкого перегляду

F5

Повторно застосувати поточний порядок

Ctrl+Q

Вийти з calibre

X

Увімкнути або вимкнути автоматичне гортання у списку книг

Ctrl+Alt+Shift+F

Обмежити показані книги тими, які належать до категорії, яку показано на панелі Навігатор мітками

B

Навігація анотаціями (підсвічуваннями і закладками), які створено у переглядачі calibre для усіх книг у бібліотеці

Ctrl+Shift+N

Навігація нотатками, які пов’язано із записами авторів, циклів, міток тощо

Alt+Shift+L

Перемкнути програму між широким і вузьким режимами

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Додавання вашого улюбленого сайта новин

Додавання вашого улюбленого сайта новин

У calibre передбачено потужну, гнучку і просту у користуванні бібліотеку для отримання новин з інтернету та перетворення даних цих новин на електронну книгу. Нижче ми покажемо вам, на прикладах, як отримувати новини з різноманітних сайтів.

Щоб дізнатися більше про те, як користуватися бібліотекою, послідовно ознайомтеся із прикладами з наведеного нижче переліку:

- Повністю автоматизоване отримання даних

- Блог calibre

- bbc.co.uk

- Налаштовування процесу отримання даних

- Використання друкованої версії bbc.co.uk

- Заміна стилів статті

- Вирізання і склеювання

- Наближений до реальності приклад

- Підказки щодо створення нових рецептів

- Додаткові матеріали

- Документація з програмного інтерфейсу

Повністю автоматизоване отримання даних

Якщо ваше джерело новин є доволі простим, calibre, ймовірно, зможе отримувати дані з нього у повністю автоматичному режимі — вам достатньо буде лише вказати адресу. calibre збирає усі дані, потрібні для дотримання даних джерела новин до рецепт`а. Щоб повідомити calibre про джерело новин, вам слід створити для нього :term:`рецепт. Розгляньмо декілька прикладів:

Блог calibre

У блозі calibre розміщують дописи, які описують для нових користувачів calibre багато корисних можливостей програми. Щоб отримати дані цього блогу у форматі електронної книги, ми скористаємося подачею RSS блогу:

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

Отримати адресу RSS можна, пошукавши у розділі «Subscribe to» у нижній частині сторінки блогу і вибравши Posts → Atom. Щоб зробити так, щоб програма calibre отримувала подачі і перетворювала вміст дописів на електронні книги, вам слід натиснути стрілку, розташовану праворуч від кнопки Отримати новини, і потім вибрати пункт меню Додати власне джерело новин, і далі натиснути кнопку Створити рецепт. У відповідь буде відкрито діалогове вікно, подібне до наведеного нижче.

Спочатку введіть Calibre Blog у поле Назва рецепта. Ця назва визначить назву електронної книги, яку буде створено на основі статей у вказаних вище подачах.

За допомогою наступних двох полів (Найстаріша стаття і Макс. кількість статей) ви можете керувати тим, скільки статей слід отримувати для кожної з подач. Призначення цих полів є доволі очевидним.

Щоб додати подачі до рецепта, вкажіть назву подачі та її адресу, а потім натисніть кнопку Додати подачу. Після додавання подачі натисніть кнопку Зберегти, і усе! Закрийте діалогове вікно.

Щоб випробувати ваш новий рецепт, натисніть кнопку Отримати новини і у підменю Нетипові джерела новин вибрати пункт calibre Blog. За декілька хвилин на панелі основної бібліотеки з’явиться щойно отримана електронна книга із дописами у блозі. Якщо з комп’ютером з’єднано ваш пристрій для читання електронних книг, книгу буде записано на пристрій замість бібліотеки. Позначте книгу і натисніть кнопку Перегляд, щоб розпочати читання!

Причиною того, що все спрацювало так добре із мінімумом зусиль, є те, що блозі використано повноформатні подачі RSS, тобто вміст статей вбудовано до самої подачі. Для більшості джерел новин, які надають дані у цей спосіб, із повноформатними подачами, вам не знадобляться ніяких зусилля для перетворення даних на електронні книги. Тепер погляньмо на джерело новин, яке не надає повноформатних подач. У подачах таких джерел повний текст новини міститься на сторінці сайта, а у подачі міститься лише посилання на сторінку із коротким резюме статті.

bbc.co.uk

Спробуємо скористатися вказаними нижче двома подачами з The BBC:

1. Перша сторінка новин: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/front_page/rss.xml

2. Наука і природа: https://newsrss.bbc.co.uk/rss/newsonline_world_edition/science/nature/rss.xml

Виконайте процедуру, яку описано вище у розділі Блог calibre, для створення рецепта для The BBC (використайте наведені вище адреси подач). Якщо поглянути на отриману електронну книгу, можна побачити, що calibre виконано значний обсяг робіт для видобування лише потрібних вам даних зі сторінок усіх статей. Втім, процедура видобування не є ідеальною. Іноді, після неї залишаються небажані дані, зокрема меню та навігаційні засоби, або вилучаються дані, які варто було б лишити, зокрема заголовки статей. Для отримання ідеального набору даних нам доведеться налаштувати процес отримання у спосіб, який описано у наступному розділі.

Налаштовування процесу отримання даних

Якщо ви хочете зробити процес отримання даних або вміст книг з якогось складного сайта ідеальним, ви можете скористатися усією потужністю та гнучкістю набору бібліотек recipe. Для цього у діалоговому вікні Додати нетипове джерело новин натисніть кнопку Перемкнутися у розширений режим.

Найпростішою і часто найпродуктивнішою зміною є використання друкованої версії інтернет-статей. Зазвичай, така друкована версія містить набагато менше зайвих частин і набагато простіше перетворюється на електронну книгу. Давайте спробуємо скористатися друкованою версією статей з The BBC.

Використання друкованої версії bbc.co.uk

Першим кроком є вивчення електронної книги, яку ми раніше отримати з bbc.co.uk. Наприкінці кожної зі статей у електронній книзі є невеличка анотація, яка повідомляє про те, звідки було отримано статтю. Скопіюйте і вставте цю адресу до вікна програми для перегляду інтернету. Тепер, на сторінці статті знайдіть посилання, яке вказує на друковану версію («Printable version»). Натисніть його, щоб переглянути друковану версію статті. Усе виглядає набагато краще! Тепер порівняймо дві адреси. У мене вони виглядають так:

Адреса статті

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

Адреса друкованої версії

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

Отже, здається, для того, щоб отримати придатну до друку версію, нам слід додати до кожної адреси статті такий префікс:

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

Тепер, у Розширеному режимі діалогового вікна нетипових джерел новин ви маєте побачити щось таке (не забудьте вибрати рецепт The BBC перед перемиканням у розширений режим):

Як бачите, поля з Базового режиму перетворилися на код Python у доволі очевидний спосіб. Нам слід додати до цього рецепту інструкції для використання друкованої версії статей. Для цього достатньо додати такі два рядки:

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

Це мова програмування Python, тому важливо використовувати відступи у рядках коду. Після додавання вами рядків, код має виглядати так:

У наведеному вище коді def print_version(self, url) визначає метод, який викликається calibre для кожної статті. url — це адреса початкової версії статті. print_version бере адресу і замінює її новою адресою, яка вказує на друковану версію статті. Щоб дізнатися більше про програмування мовою Python [https://www.python.org], ознайомтеся з підручником [https://docs.python.org/tutorial/].

Тепер натисніть кнопку Додати/Оновити рецепт, і внесені вами зміни буде збережено. Повторно отримайте книгу. Тепер вона має бути набагато ліпшою. Однією з проблем у новій версії є те, що типові шрифти друкованої версії сторінки є надто малими. Це автоматично виправляється під час перетворення даних на електронну книгу, але навіть після виправлення розмір шрифту меню і панелі навігації є набагато більшим за розмір шрифту тексту статті. Щоб виправити це, ми реалізуємо певні додаткові виправлення у наступному розділі.

Заміна стилів статті

У попередньому розділі ми бачили, що розмір шрифту для статей у версії для друку сторінок The BBC є надто малим. На більшості сайтів, зокрема і на The BBC, цей розмір шрифту встановлюється таблицями стилів CSS. Ви можете вимкнути отримання таких таблиць стилів додаванням такого рядка:

no_stylesheets = True

Тепер рецепт виглядає так:

Нова версія виглядає достатньо якісною. Якщо вам завжди хочеться повної досконалості, вам варто ознайомитися із наступним розділом, де обговорено внесення змін до отриманих даних.

Вирізання і склеювання

У calibre передбачено дуже потужні і гнучкі можливості щодо обробки отриманих даних. Щоб продемонструвати частину цих можливостей, звернімося до нашого старого доброго рецепта The BBC. Погляньмо на початковий код (HTML) статей (версії для друку). Як можна помітити, у них є нижній колонтитул, у якому не міститься корисної інформації. Він є частиною

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

Його можна вилучити додаванням такого коду:

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

до рецепта. Нарешті, давайте замінимо частину CSS, які ми вимкнули раніше, нашими власними CSS, які придатні для застосування перетворення до книги:

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

Із цими доповненнями наш рецепт набув «промислової якості».

Цей рецепт використовує лише верхівку айсберга, якщо йдеться про потужність calibre. Щоб познайомитися із ширшим спектром можливостей calibre, ми вивчимо складніший приклад з реального життя у наступному розділі.

Наближений до реальності приклад

Прийнятно складним наближеним до реальності прикладом, який продемонструє ширші можливості програмного інтерфейсу BasicNewsRecipe є рецепт для The New York Times
import string, re from calibre import strftime from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.ebooks.BeautifulSoup import BeautifulSoup class NYTimes(BasicNewsRecipe):

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

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

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

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

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

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

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

У цьому :term:`рецепт`і ми познайомимося із декількома новими можливостями. На початку маємо:

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

Це встановлює формат показаного часу на першій сторінці створеної електронної книги День, Номер_дня Місяць, Рік. Див. timefmt.

Далі йде група інструкцій для чищення отриманих даних HTML:

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

Ці команди вилучають усе до першого теґу <h1> і усе після першого теґу, чиїм ідентифікатором є footer. Див. remove_tags, remove_tags_before, remove_tags_after.

Ще одна цікава можливість:

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

needs_subscription = True повідомляє calibre, що рецепту потрібні ім’я користувача і пароль для доступу до даних. Використання цього значення спричиняє у calibre появу діалогового вікна запиту щодо імені користувача і пароля кожного разу, коли ви намагатиметеся скористатися цим рецептом. Код у calibre.web.feeds.news.BasicNewsRecipe.get_browser() виконує дії, які потрібні для того, щоб увійти як зареєстрований користувача на сайт NYT. Після входу calibre використає той самий, уповноважений екземпляр програми для перегляду інтернету для отримання усіх даних. Щоб краще зрозуміти код у get_browser, див mechanize [https://mechanize.readthedocs.io/en/latest/].

Наступною новою можливістю є метод calibre.web.feeds.news.BasicNewsRecipe.parse_index(). Його завданням є перехід на https://www.nytimes.com/pages/todayspaper/index.html і отримання списку статей, які з’являються у сьогоднішній газеті. Хоча рецепт є складнішим за просте використання RSS, він створює електронну книгу, яка дуже схожа на щоденну газету. parse_index широко використовує BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] для обробки сторінки щоденної газети. Ви можете скористатися іншими, новішими обробниками, якщо вам не подобається BeautifulSoup. У calibre вбудовано lxml [https://lxml.de/] та html5lib [https://github.com/html5lib/html5lib-python], які є рекомендованими засобами обробки. Щоб скористатися ними, замініть виклик index_to_soup() на такий код:

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)

Останньою новою можливістю є метод calibre.web.feeds.news.BasicNewsRecipe.preprocess_html(). Цим методом можна скористатися для виконання довільних перетворень на кожною отриманою сторінкою HTML. Тут його використано для того, щоб обійти рекламні повідомлення, які nytimes показує перед кожною статтею.

Підказки щодо створення нових рецептів

Найкращим інструментом під час розробки нових рецептів є інтерфейс командного рядка. Ви можете створити рецепт за допомогою вашого улюбленого редактора коду мовою Python і зберегти його до файла, скажімо myrecipe.recipe. Суфіксом назви файла має бути обов’язково .recipe. Далі, ви можете отримати дані за допомогою цього рецепта, скориставшись такою командою:

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

Команда ebook-convert отримає усі сторінки сайта і збереже їх до файла EPUB myrecipe.epub. Параметр -vv наказує ebook-convert вивести багато даних щодо того, які дії виконуються. Параметр ebook-convert-recipe-input --test наказує програмі отримати лише пару статей із не більше двох подач. Крім того, ebook-convert розташує отриманий файл HTML у теці debug/input, де debug є текою, який вказано у аргументі параметра ebook-convert --debug-pipeline.

Щойно отримання даних буде завершено, ви зможете переглянути отриманий HTML, відкривши файл debug/input/index.html у програмі для перегляду сторінок інтернету. Коли вас задовольнять результати отримання і попередньої обробки, ви можете створити електронні книги у різних форматах ось так:

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

Якщо робота рецепта задовольняє ваші потреби і вам здається, що рецепт варто включити до набору вбудованих рецептів, розмістіть ваш рецепт на форумі рецептів calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228], щоб його роботу могли обговорити інші користувачі calibre.

Примітка

У macOS засоби керування за допомогою командного рядка є частиною пакунка calibre. Наприклад, якщо calibre встановлено до /Applications, засоби керування за допомогою командного рядка зберігатимуться у каталозі /Applications/calibre.app/Contents/MacOS/.

Дивись також

ebook-convert

Інтерфейс командного рядка для перетворення усіх книг.

Додаткові матеріали

Щоб дізнатися більше про написання складних рецептів з використанням можливостей, доступ до яких надає BasicNewsRecipe, вам слід ознайомитися з такими джерелами:

Документація з програмного інтерфейсу

Документація з класу «BasicNewsRecipe» та усіх його важливих методів та полів.

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

Початковий код «BasicNewsRecipe`»

Вбудовані рецепти [https://github.com/kovidgoyal/calibre/tree/master/recipes]

Початковий код вбудованих рецептів, які постачаються разом із calibre

Форум рецептів calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228]

Там ви зможете обговорити різні питання із багатьма досвідченими творцями рецептів для calibre.

Документація з програмного інтерфейсу

Документація з програмного інтерфейсу для роботи з рецептамиBasicNewsRecipe

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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Додавання вашого улюбленого сайта новин »

- Документація з програмного інтерфейсу для роботи з рецептами

Документація з програмного інтерфейсу для роботи з рецептами

Програмний інтерфейс для написання скриптів визначено класом BasicNewsRecipe
class calibre.web.feeds.news.BasicNewsRecipe(options, log, progress_reporter)[source]

Базовий клас, який містить логіку, потрібну для усіх рецептів. Поступово перевизначаючи функціональність у цьому класі ви можете створити поступово налаштовувані або потужніші рецепти. Початкові настанови щодо створення рецептів можна знайти у розділі Додавання вашого улюбленого сайта новин.

abort_article(msg=None)[source]

Цей метод слід викликати у будь-якому з методів попередньої обробки для переривання отримання поточної статті. Корисний для пропускання статей, які містять неприйнятні дані, зокрема статей, які містять лише відео.

abort_recipe_processing(msg)[source]

Спричиняє переривання завантаження цього рецепта системою отримання рецепта, показуючи просте повідомлення-відповідь користувачеві.

add_toc_thumbnail(article, src)[source]

Викликається з populate_article_metadata із атрибутом src теґу <img> зі статті, яку слід використовувати як мініатюру, що відповідає статті у таблиці вмісту. Мініатюра, яку насправді буде використано залежить від пристрою (у поточній версії використовується лише у пристроях Kindle). Зауважте, що еталонне зображення має бути серед тих, які було успішно отримано, інакше його буде проігноровано.

classmethod adeify_images(soup)[source]

Якщо ваш рецепт після перетворення на формат EPUB має проблеми із зображеннями під час перегляду у Adobe Digital Editions, викличте цей метод з postprocess_html().

canonicalize_internal_url(url, is_link=True)[source]

Повертає набір канонічних представлень url. У типовій реалізації використовується лише назва вузла сервера та шлях адреси, ігноруються усі параметри запиту, фрагменти тощо. Канонічні представлення мають бути унікальними щодо усіх адрес у джерелі новин. Якщо вони не є такими, внутрішні посилання можуть визначатися із помилками.

Параметри:

is_link – Має значення True, якщо адреса надходить із зовнішнього посилання у файлі HTML. False, якщо адреса є адресою, що використовується для отримання статті.

cleanup()[source]

Викликається після отримання усіх статей. Скористайтеся ним для виконання будь-якого спорожнення, зокрема виходу з облікового запису на сайтах з підпискою тощо.

clone_browser(br)[source]

Клонувати програму для перегляду інтернету br. Клоновані програми для перегляду інтернету використовуються для багатопотокового отримання даних, оскільки mechanize не є безпечним щодо виконання у потоках. Типові процедури клонування мають підхопити більшу частину з налаштувань програми для перегляду інтернету, але якщо ви робите у вашому рецепті щось екзотичне, вам слід перевизначити цей метод у вашому рецепті і клонувати програму вручну.

Клоновані екземпляри програми для перегляду інтернету типово використовує той самий, безпечний щодо потоків виконання CookieJar, якщо вами не визначено нетипову обробку кук.

default_cover(cover_file)[source]

Створити типову обкладинку для рецептів, у яких немає обкладинки

download()[source]

Отримати і попередньо обробити усі статті з подачі у цьому рецепті. Цей метод слід викликати лише один раз у одному екземплярі рецепта. Повторне викликання методу призведе до невизначеної поведінки. Повертає шлях до index.html

extract_readable_article(html, url)[source]

Видобуває основні дані статті з «html», вилучає зайві дані і повертає кортеж (article_html, extracted_title). Засновано на початковому алгоритмі читання від Arc90.

get_article_url(article)[source]

Перевизначте у підкласі для зміни поведінки видобування URL, який вказує на вміст кожної статті. Повертає адресу статті. Викликається з параметром article, об’єктом, який відповідає обробленій статті з подачі. Див. feedparser [https://pythonhosted.org/feedparser/]. Типово, шукає початкове посилання (для подач, що розповсюджуються за допомогою служб, зокрема FeedBurner або Pheedo) і, якщо знайдено, повертає його. Інакше повертає article.link [https://pythonhosted.org/feedparser/reference-entry-link.html].

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

Повертає екземпляр програми для перегляду інтернету, що використовується для отримання документів з інтернету. Типово, повертає екземпляр програми mechanize [https://mechanize.readthedocs.io/en/latest/], у якому передбачено підтримку кук, ігнорування robots.txt, обробку оновлень та агент користувача mozilla firefox.

Якщо ваш рецепт потребує початкового входу до системи, перевизначте цей метод у вашому підкласі. Наприклад, наведений нижче код використовується у рецепті для New York Times для входу і отримання повноцінного доступу:

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]

Повертає URL зображення обкладинки для цього випуску або None. Типово, повертає значення member self.cover_url, яким є зазвичай None. Якщо ви хочете, щоб ваш рецепт отримував зображення обкладинки для електронної книги, перевизначте цей метод у вашому підкласі або встановіть змінну member self.cover_url до виклику цього методу.

get_extra_css()[source]

Типово повертає «self.extra_css». Перевизначте, якщо вам потрібно, щоб extra_css було створено програмно.

get_feeds()[source]

Повертає список подач RSS для отримання для цього профілю. Кожен елемент списку має бути двоелементним кортежем у формі (назва, адреса). Якщо значенням назви є None або порожній рядок, буде використано назву з подачі. Цей метод є корисним, якщо ваш рецепт потребує певної обробки для визначення списку подач, дані яких слід отримувати. Якщо ви маєте справу з саме таким джерелом даних, перевизначте метод у вашому підкласі.

get_masthead_title()[source]

Перевизначте у підкласі, щоб скористатися чимось іншим, окрім заголовка рецепта.

get_masthead_url()[source]

Повертає URL зображення титульних даних для цього випуску або None. Типово, повертає значення member self.masthead_url, яким типово є None. Якщо вам потрібно, щоб ваш рецепт отримував титульні дані для електронної книги, перевизначте цей метод у вашому підкласі або встановіть значення змінної member self.masthead_url до виклику цього методу. Зображення титульних даних використовуються у файлах MOBI Kindle.

get_obfuscated_article(url)[source]

Якщо ви встановите articles_are_obfuscated, цей метод буде викликано для усіх адрес статей. Він має повертати шлях у файловій системі до файла, який містить код HTML статті. Цей файл буде оброблено рушієм рекурсивного отримання даних HTML, отже він може містити посилання на сторінки і зображення в інтернеті. Крім того, можна повертати словник у формі {„data“: <HTML data>, „url“: <the resolved URL of the article>}. Таким чином можна уникнути створення тимчасових файлів. Ключ url у цьому словнику корисних, якщо сама адреса статті відрізняється від адреси, яку передано до цього методу, наприклад, через переспрямовування. Його можна пропустити, якщо адреса лишається незмінною.

Типово, цей метод є корисним для сайтів, які ускладнюють автоматичний доступ до вмісту статей.

get_url_specific_delay(url)[source]

Повертає затримку у секундах перед отриманням даних з цієї адреси. Якщо ви хочете програмно задати затримку для визначеної адреси, перевизначте цей метод у вашому підкласі, повертаючи типово self.delay для адрес, на які цей параметр не повинен вплинути.

Повертає:

Число з рухомою крапкою, затримка у секундах.

classmethod image_url_processor(baseurl, url)[source]

Виконує деяку обробку адрес зображень (вилучення обмежень за розміром для динамічно створених зображень тощо) і повертає оброблену адресу. Повертає None або порожній рядок, якщо отримання зображення слід пропустити.

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

Зручний метод, який отримує адресу основної сторінки і повертає створений на її основі BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc].

«url_or_raw»: адреса або отримана сторінка покажчика як рядок

is_link_wanted(url, tag)[source]

Повертає True, якщо за посиланням слід перейти, або False, якщо ні. Типово, видає NotImplementedError, що призводить до ігнорування посилання засобом отримання даних.

Параметри:

- url – Адреса для переходу

- tag – Мітка, за якою було визначено адресу.

parse_feeds()[source]

Створює список статей на основі списку подач, повернутого BasicNewsRecipe.get_feeds(). Повертає список об’єктів Feed.

parse_index()[source]

Цей метод слід реалізовувати у рецептах, які обробляють дані сайта замість подач для створення списку статей. Типовим використанням є, обробка джерел новин, які мають вебсторінки «Друкованої версії» зі списками усіх статей у поточному друкованому виданні. Якщо цю функцію реалізовано, вона матиме вищий пріоритет використання за BasicNewsRecipe.parse_feeds().

Має повертати список. Кожен елемент списку має бути двоелементним кортежем у формі ('назва подачі', список статей).

Кожен список статей має містити словники у такій формі:

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

Приклад можна знайти у рецепті для отримання The Atlantic. Крім того, ви можете додати «author» для автора статті.

Якщо ви з якоїсь причини хочете перервати обробку і хочете наказати calibre показати користувачеві просте повідомлення замість повідомлення про помилку, викличте abort_recipe_processing().

populate_article_metadata(article, soup, first)[source]

Викликається під час отримання кожної зі сторінок HTML, що належать до статті. Призначено для отримання метаданих статті, зокрема даних щодо автора, резюме із обробленого HTML (soup).

Параметри:

- article – об’єкт класу calibre.web.feeds.Article. Якщо ви зміните summary, не забудьте також змінити text_summary.

- soup – Оброблений HTML, що відповідає цій статті.

- first – Має значення True лише тоді, коли оброблений HTML є першою сторінкою статті.

postprocess_book(oeb, opts, log)[source]

Виконати будь-яку остаточну обробку для отриманої електронної книги.

Параметри:

- oeb – Об’єкт OEBBook

- opts – Параметри перетворення

postprocess_html(soup, first_fetch)[source]

Цей метод викликається із джерелом кожного отриманого файла HTML після його обробки для отримання посилань та зображень. Ним можна скористатися для виконання довільної потужної остаточної обробки коду HTML. Має повертати soup після завершення обробки.

Параметри:

- soup – Екземпляр BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/], що містить отриманий код HTML.

- first_fetch – True, якщо це перша сторінка статті.

preprocess_html(soup)[source]

Цей метод викликається із джерелом кожного отриманого файла HTML до його обробки для отримання посилань та зображень. Викликається після чищення коду, яке визначається методами remove_tags тощо. Ним можна скористатися для виконання довільної потужної попередньої обробки коду HTML. Має повертати soup після завершення обробки.

Екземпляр BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/], що містить отриманий код HTML.

preprocess_image(img_data, image_url)[source]

Виконати певну обробку отриманих даних зображення. Викликається для необроблених даних до виконання усіх дій зі зміни розмірів. Має повертати оброблені дані. Повертає None, якщо зображення слід пропустити.

preprocess_raw_html(raw_html, url)[source]

Цей метод викликається для джерела кожного отриманого файла HTML до того, як з нього буде створено дерево об’єктів. raw_html є рядком у кодуванні unicode, що відповідає необробленому коду HTML, який отримано з інтернету. url — адреса, з якої було отримано код HTML.

Зверніть увагу на те, що цей метод діє до preprocess_regexps.

Цей метод повинен повернути оброблений raw_html як об’єкт Unicode.

classmethod print_version(url)[source]

Приймає url, адресу, яка вказує на сторінку із вмістом статті, і повертає URL, адресу, що вказує на друковану версію статті. Типово не виконує ніяких дій. Приклад:

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

publication_date()[source]

Скористайтеся цим методом для встановлення дати публікації цього випуску. Типовим є момент отримання. Має повертати об’єкт datetime.datetime.

skip_ad_pages(soup)[source]

Цей метод викликається для джерела кожного отриманого файла HTML до застосування будь-яких атрибутів для чищення, зокрема remove_tags, keep_only_tags тощо. Зауважте, що preprocess_regexps буде застосовано до застосування цього методу. Метод призначено для того, щоб рецепт міг пропустити сторінки реклами. Якщо soup відповідає сторінці реклами, повертає HTML справжньої сторінки даних. Інакше повертає None.

Екземпляр BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/], що містить отриманий код HTML.

sort_index_by(index, weights)[source]

Зручний метод для упорядковування заголовків у index відповідно до weights. index — упорядковується на місці. Повертає index.

index: Список назв.

weights: словник для ваги заголовків. Якщо заголовки у index не буде знайдено у weights, для них буде визначено вагу 0.

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

Зручний метод для отримання Tag BeautifulSoup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/] і рекурсивного видобування з нього тексту, зокрема усіх розділів CDATA і атрибутів alt теґів. Повертає рядок Unicode, який може бути порожнім.

use_alt: якщо True, спробувати скористатися атрибутом alt для теґів, у яких немає жодного текстового вмісту.

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

articles_are_obfuscated = False

Встановіть значення True і реалізуйте get_obfuscated_article() для обробки сайтів, у яких вжито заходи для ускладнення отримання даних.

auto_cleanup = False

Автоматично видобуває увесь текст із отриманих сторінок статей. Використовує алгоритми з проекту readability. Встановлення для цього параметра значення True, означає, що ви не хочете перейматися чищенням отриманого коду HTML вручну (хоча таке чищення завжди даватиме кращі результати).

auto_cleanup_keep = None

Визначає елементи, які алгоритм автоматичного чищення ніколи не повинен вилучати. Синтаксично є виразом XPath. Приклад:

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

Якщо True, панель навігації буде вирівняно за центром, інакше його буде вирівняно ліворуч.

compress_news_images = False

Встановіть для цього параметра значення False, якщо ви хочете, щоб усі параметри масштабування і стискання було проігноровано, отже усі зображення передано без змін. Якщо має значення True і для усіх інших параметрів стискання залишено типові значення, зображення JPEG буде масштабовано відповідно до розмірностей екрана, які встановлено профілем виведення даних і стиснуто до розміну, що не перевищує (ш * в)/16, де ш x в є шириною і висотою масштабованого зображення.

compress_news_images_auto_size = 16

Коефіцієнт, що використовуватиметься для автоматичного стискання зображень JPEG. Якщо встановлено значення None, автоматичне стискання буде вимкнено. Якщо встановлено інше значення, зображення буде зменшено у розмірах до (ш * в)/compress_news_images_auto_size байтів, якщо це можливо, зменшенням рівня якості. Тут ш x в — ширина і висота зображення у пікселях. Мінімальним значенням якості JPEG є 5/100, отже, може так статися, що бажаних обмежень досягти не вдасться. Значення цього параметра можна перевизначити за допомогою параметра compress_news_images_max_size, який визначає фіксований максимальний розмір зображень. Зауважте, що якщо ви увімкнете scale_news_images_to_device, зображення буде спочатку масштабовано, а потім його якість буде знижено, аж доки розмір його даних стане меншим за (ш * в)/коефіцієнт, де ш — ширина, а в — висота масштабованого зображення. Іншими словами, стискання відбуватиметься після масштабування.

compress_news_images_max_size = None

Встановити якість стискання JPEG так, щоб розмір файлів зображень не перевищував вказаний (у кілобайтах). Якщо встановлено, цей параметр перевизначає параметр автоматичного стискання, який задано compress_news_images_auto_size. Мінімальним значенням якості JPEG є 5/100, тому можуть бути випадки, коли задовольнити обмеження все ж не вдасться.

conversion_options = {}

Специфічні для рецепта параметри, які керують перетворенням отриманих даних книги. Перевизначають усі задані користувачем або додатком значення, тому їх слід використовувати лише за кончої потреби. Приклад:

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

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

Типово, як зображення обкладинки періодичного видання буде використано зображення обкладинки, яке повертає get_cover_url(). Перевизначення цього у вашому рецепті накаже calibre обробити отримане зображення обкладинки так, щоб воно вклалося у прямокутник, ширина і висота якого визначаються у відсотках до ширини і висоти отриманого зображення обкладинки. Значення cover_margins = (10, 15, „#ffffff“) додає до обкладинки поле у 10 пікселів ліворуч і праворуч і у 15 пікселів згори і знизу. Назви кольорів визначено тут [https://www.imagemagick.org/script/color.php]. Зауважте, що з певної причини значення white не завжди спрацьовує у Windows. Використовуйте замість назви білого кольору його значення, #ffffff

delay = 0

Типова затримка між послідовними сеансами отримання даних у секундах. Для точнішого визначення часу можна скористатися аргументом у форматі числа із рухомою крапкою. Див. get_url_specific_delay() щодо реалізації затримок для окремих адрес.

description = ''

Декілька рядків, які описують дані, які отримуються за допомогою цього рецепта. Їх, в основному, буде використано у графічному інтерфейсі, де показується список рецептів.

encoding = None

Вказати кодування для сайтів із помилковою специфікацією кодування. Найпоширенішим варіантом для англомовних сайтів є набір символів latin1 і кодування cp1252. Якщо вказано значення None, програма спробує автоматично визначити кодування. Якщо вказано назву функції, її буде викликано із двома аргументами: об’єктом рецепта і початковими даними для декодування. Функція має повертати декодовані дані.

extra_css = None

Визначає будь-яку додаткову таблицю стилів CSS, яку слід додати до отриманих файлів HTML. Її буде вставлено до теґів <style>, одразу перед завершальним теґом </head>, і таким чином перевизначено усі CSS, окрім тієї, яку оголошено за допомогою атрибута style для окремих теґів HTML. Зауважте, що якщо ви хочете програмовим чином створити extra_css, вам слід перевизначити замість цього методу метод get_extra_css(). Приклад:

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

feeds = None

Список подач для отримання. Можна задавати у форматі [адреса1, адреса2, ...] або у форматі [('назва1', адреса1), ('назва2', адреса2),...]

filter_regexps = []

Список формальних виразів, який визначає посилання, які слід ігнорувати. Якщо список є порожнім, його буде проігноровано. Використовується, лише якщо не реалізовано is_link_wanted. Приклад:

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

вилучити усі адреси, які містять ads.doubleclick.net.

Має бути визначено лише один з атрибутів, BasicNewsRecipe.match_regexps або BasicNewsRecipe.filter_regexps.

handle_gzip = True

Встановіть значення False, якщо ви не хочете стискати передані дані за допомогою gzip. Зауважте, що деякі застарілі сервери не сприймають стиснені gzip дані.

ignore_duplicate_articles = None

Ігнорувати дублікати статей, які є у декількох розділах. Стаття-дублікат є статей, яка має той самий заголовок і/або адресу. Щоб програма ігнорувала статті з однаковими заголовками, встановіть таке значення:

ignore_duplicate_articles = {'title'}

Щоб використовувати адреси, встановіть таке:

ignore_duplicate_articles = {'url'}

Щоб встановлювати відповідність за заголовком або адресою, встановіть таке:

ignore_duplicate_articles = {'title', 'url'}

keep_only_tags = []

Зберегти лише вказані теґи і їхні дочірні теґи. Формат визначення теґу описано у розділі щодо BasicNewsRecipe.remove_tags. Якщо цей список не є порожнім, теґ <body> буде спорожнено і повторно заповнено теґами, які відповідають записам у цьому списку. Приклад:

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

збереже лише теґи, у яких є атрибут id у «content» або «heading».

language = 'und'

Мова, якою написано новини. Визначається за допомогою дво- або трилітерного коду ISO-639

masthead_url = None

Типово, calibre використовуватиме типове зображення для masthead (лише Kindle). Перевизначте його у вашому рецепті, щоб надати адресу, яку слід використовувати як masthead.

match_regexps = []

Список формальних виразів, який визначає посилання, за якими слід переходити. Якщо список є порожнім, його буде проігноровано. Використовується, лише якщо не реалізовано is_link_wanted. Приклад:

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

буде встановлено відповідність усіх адрес, які містять вираз «page=якесь число».

Має бути визначено лише один з атрибутів, BasicNewsRecipe.match_regexps або BasicNewsRecipe.filter_regexps.

max_articles_per_feed = 100

Максимальна кількість статей для отримання з кожної з подач. В основному, корисна для подач, у яких не вказано дат статей. Для більшості статей вам слід використовувати BasicNewsRecipe.oldest_article

needs_subscription = False

Якщо True, у графічному інтерфейсі програма попросить користувача вказати ім’я і пароль, які слід використовувати для отримання даних. Якщо встановлено як «optional», зазначення імені користувача і пароля буде необов’язковим.

no_stylesheets = False

Зручний прапорець для вимикання завантаження таблиць стилів для сайтів, у яких використовуються надто складні таблиці стилів, які є непридатними для перетворень на формати електронних книг. Якщо має значення True, таблиці стилів не отримуються і не обробляються.

oldest_article = 7.0

Вік найстарішої статті, яку слід завантажувати для цього нового джерела новин. Визначається у днях.

preprocess_regexps = []

Список правил підставляння regexp, якими слід скористатися для отриманого коду HTML. Кожен запис у списку має бути двоелементним кортежем. Першим елементом кортежу має бути скомпільований формальний вираз, а другим — функція, яка приймає одинарний об’єкт-відповідник і повертає рядок, яким слід замінити відповідник. Приклад:

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

вилучить усе від <!–Article ends here–> до </body>.

publication_type = 'unknown'

Тип публікації. Встановіть у значення newspaper, magazine або blog. Якщо встановлено None, до файла opf не буде записано метаданих типу публікації.

recipe_disabled = None

Встановіть непорожнє рядкове значення, щоб вимкнути цей рецепт. Рядок буде використано замість повідомлення про вимикання.

recursions = 0

Кількість рівнів посилань, за якими слід переходити на сторінках статті.

remove_attributes = []

Список атрибутів для вилучення з усіх теґів. Приклад:

remove_attributes = ['style', 'font']

remove_empty_feeds = False

Якщо має значення True, з виведення буде прибрано порожні подачі. Цей параметр не спрацює, якщо parse_index перевизначено у підкласі. Його призначено лише для рецептів, які повертають список подач за допомогою feeds або get_feeds(). Також використовується, якщо використано параметр ignore_duplicate_articles.

remove_javascript = True

Зручний прапорець для усування усіх теґів JavaScript з отриманого HTML

remove_tags = []

Список теґів для вилучення. Вказані теґи буде вилучено з отриманого HTML. Теґ вказується як словник у такій формі:

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

Усі ключі є необов’язковими. Повний опис критеріїв пошуку можна знайти у розділі Beautiful Soup [https://www.crummy.com/software/BeautifulSoup/bs4/doc/#searching-the-tree]. Типовий приклад:

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

Це вилучить усі теґи <div class=»advert»> і усі дочірні теґи з отриманого HTML.

remove_tags_after = None

Вилучити усі теґи, які буде знайдено після вказаного теґу. Щоб дізнатися більше про формат визначення теґу, див. розділ щодо BasicNewsRecipe.remove_tags. Приклад:

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

вилучить усі теґи після першого елемента з id=»content».

remove_tags_before = None

Вилучити усі теґи, які буде знайдено до вказаного теґу. Щоб дізнатися більше про формат визначення теґу, див. розділ щодо BasicNewsRecipe.remove_tags. Приклад:

remove_tags_before = dict(id='content')

вилучить усі теґи до першого елемента з id=»content».

requires_version = (0, 6, 0)

Мінімальна версія calibre, потрібна для використання цього скрипту

resolve_internal_links = False

Якщо встановлено значення True, посилання у отриманих статтях на інші отримані статті замінюються так, щоб вони вказували на отриману копію статті, а не на її оригінал у інтернеті. Якщо ви встановите значення True, вам також варто реалізувати метод canonicalize_internal_url(), щоб мати змогу обробляти схему адрес відповідного сайта.

reverse_article_order = False

Змінити порядок статей в усіх подачах на зворотний

scale_news_images = None

Максимальні розміри (w,h), до яких слід масштабувати зображення. Якщо scale_news_images_to_device має значення True, профілем виведення даних буде встановлено розмірності екрана пристрою, якщо такий профіль встановлено. Якщо профіль не встановлено, значення змінено не буде (типовим значенням є None).

scale_news_images_to_device = True

Змінити масштаб зображень так, щоб воно відповідало розмірам екрана пристрою, визначеним пристроєм виведення. Ігнорується, якщо профіль виведення не встановлено.

simultaneous_downloads = 5

Кількість одночасних сеансів отримання даних. Встановіть значення 1, якщо сервер працює не як слід. Автоматично зменшується до 1, якщо BasicNewsRecipe.delay > 0

summary_length = 500

Максимальна кількість символів у короткому описі.

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, яка використовується для шаблонів, тобто для панелей навігації та таблиці змісту. Замість перевизначення цієї змінної, вам слід скористатися у вашому рецепті extra_css для налаштовування вигляду та поведінки.

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

Рядок формату для дати, показаної на першій сторінці. Типово: Назва_Дня, Номер_Дня Назва_Місяця Рік

timeout = 120.0

Час очікування на отримання файлів із сервера, у секундах

title = 'Невідоме джерело новин'

Заголовок, який слід використати для книги

use_embedded_content = None

Зазвичай, ми намагаємося встановити, чи містить подача повні тексти статей на основі об’єму вбудованих даних. Якщо має значення None, буде використано типовий спосіб визначення. Якщо має значення True, програма завжди вважатиме, що у подачу вбудовано дані. Якщо ж має значення False, програма завжди вважатиме, що у подачі не міститься вмісту статей.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Переглядач електронних книжок

Переглядач електронних книжок
calibre включає вбудований переглядач електронних книжок, який дозволяє переглядати більшість форматів цих книжок. Переглядач легко налаштовується та має багато функцій.

- Запуск програми для перегляду електронних книг

- Навігація електронною книгою

- Підсвічування тексту

- Прочитати уголос

- Пошук тексту

- Перехід за посиланнями лише за допомогою клавіатури

- Налаштовування вигляду і поведінки програми під час читання

- Пошук у словнику

- Копіювання тексту та малюнків

- Збільшення масштабу зображень

- Клавіатурні скорочення

- Непридатний до перенесення текст

- Компонування вашої книги для роботи із переглядачем calibre

Запуск програми для перегляду електронних книг

Ви можете переглядати будь-які книжки з вашої бібліотеки calibre, вибравши книжку та натиснувши кнопку Переглянути. У відповідь книгу буде відкрито у програмі для перегляду. Також ви можете запустити саму програму для перегляду книг за допомогою меню Windows. У macOS, ви можете пришпилити кнопку програми на бічну панель і запускати програму за допомогою цієї кнопки. У Linux ви можете скористатися засобом запуску у меню стільниці або командою ebook-viewer.

Навігація електронною книгою

«Гортати» сторінки у книзі можна так:

- Клацанням кнопкою миші на лівому або правому полі сторінки

- Натисканням клавіш Пробіл, Page Up, Page Down або клавіш зі стрілками

- На сенсорному екрані торканням тексту або проведенням пальцем по екрану ліворуч або праворуч

Доступ до інструментів керування переглядом можна отримати так:

- Клацанням правою кнопкою миші на тексті

- Натисканням клавіші Esc або Menu

- Торканням до верхньої третини екрана на сенсорному екрані

Засіб перегляду книг може працювати у двох режимах — із поділом на сторінки і неперервному режимі. У режимі поділу на сторінки вміст книги буде представлено у форматі сторінок, подібно до звичайної паперової книги. У режимі неперервного перегляду текст буде показано на неперервному «сувої», подібно до тексту у браузері. Перемикатися між режимами можна за допомогою панелі Налаштування засобу перегляду, а саме, розділу Компонування сторінки, або за допомогою натискання комбінації клавіш Ctrl+M.

Закладки

Якщо ви почали читати книгу, а потім закрили вікно засобу перегляду, програма запам’ятає місце, на якому ви зупинилися, і повернеться до нього, коли ви відкриєте книгу наступного разу. Крім того, ви можете встановити у книзі закладки за допомогою кнопки Закладки на панелі засобів керування переглядом або комбінації клавіш Ctrl+B. Якщо ви читаєте книгу у форматі EPUB, закладки зберігатимуться у самому файлі EPUB. Ви можете додати закладки, а потім надіслати файл другові чи подрузі. Коли надісланий файл буде відкрито, читач зможе побачити ваші закладки. Ви можете вимкнути цю поведінку у розділі Різне у налаштуваннях засобу перегляду.

Зміст

Якщо у книзі, яку ви читаєте, є таблиця вмісту, ви можете відкрити її натисканням кнопки Зміст. У відповідь програма покаже список розділів книги. Ви можете натиснути будь-який з пунктів цього списку, щоб перейти безпосередньо до відповідної частини книги.

Навігація за місцем

У електронній книзі, на відміну від звичайної, немає сторінок. Ви можете перейти до якогось певного місця у електронній книзі за допомогою пункту меню Перейти до  →  Місце на панелі керування переглядом.

Ви можете скористатися цими даними щодо місця для створення однозначних посилань на частини книги, коли обговорюватимете вміст книги з друзями або посилатиметесь на неї у інших роботах. Ви можете ввести дані цих місць за допомогою пункту Перейти до  →  Місце на панелі засобів керування переглядом.

Маємо адресу, яку ви можете скопіювати до буфера обміну даними і вставити до інших програм або документів. Натискання пункту цієї адреси призведе до відкриття книги у засобі перегляду calibre на поточній позиції.

Якщо під час читання ви скористаєтеся посиланнями у електронній книзі, які переноситимуть вас до інших частин книги, наприклад кінцевої примітки, ви зможете за допомогою кнопок Назад і Вперед швидко переходити між початковою частиною книги і частиною посилання. Ці кнопки розташовано у верхньому лівому куті панелі засобів керування переглядом. Вони працюють так само, як відповідні кнопки програми для перегляду сторінок інтернету.

Режим посилань

Крім того, у calibre передбачено дуже зручний режим посилань. Увімкнути його можна натисканням кнопки Режим посилань. Після вмикання цього режиму на початку кожного абзацу буде показано унікальний номер, що складатиметься із номеру розділу та номеру абзацу у ньому.

Ви можете скористатися цим номером для створення однозначних посилань на частини книги, коли обговорюватимете вміст книги з друзями або посилатиметесь на неї у інших роботах. Ви можете ввести показані числа у поле із підписом Перейти у верхній частині вікна програми, щоб перейти до відповідного місця у книзі.

Підсвічування тексту

Коли ви позначаєте текст у засобі перегляду, програма показує невеличку контекстну панель поряд із позначеним фрагментом. Ви можете натиснути кнопку підсвічування на цій панелі, щоб створити підсвічування. На сенсорних екранах можна натиснути слово і потримати палець протягом певного часу, щоб програма показали контекстну панель. У режимі підсвічування ви можете змінити позначення тексту за допомогою зручних сенсорних засобів пересування меж позначеного. Перетягніть елементи керування до верхнього або нижнього поля екрана, щоб виконати гортання зображення під час позначення. Ви також можете утримувати натиснутою клавішу Shift і клацати кнопкою миші або скористатися клацанням правою кнопкою миші для розширення вибору. Це, зокрема, корисно для позначення тексту на декількох сторінках одночасно.

Ви можете скористатися кнопкою Підсвічування на панелі керування засобом перегляду, щоб відкрити окрему панель зі упорядкованим за розділами списком усіх підсвічувань у книзі.

Щоб переглянути усі підсвічування в усій вашій бібліотеці calibre, клацніть правою кнопкою миші на кнопці Перегляд і виберіть Навігація анотаціями.

Нарешті, якщо ви користуєтеся засобом перегляду у браузері сервера даних, ви можете синхронізувати анотації стандартного засобу перегляду і засобу перегляду у браузері: відкрийте сторінку Налаштування  →  Різне у вікні засобу перегляду і введіть ім’я користувача сервера даних, з обліковим записом якого виконуватиметься синхронізація. Скористайтеся особливим значенням *, якщо хочете виконувати синхронізацію із анонімними користувачами.

Прочитати уголос

У засобі для читання книг передбачено можливість читання книг уголос. Щоб скористатися нею, просто натисніть кнопку Читати уголос на панелі керування засобом перегляду, і програма почне читання тексту уголос. Поточне слово, яке читатиме програма, буде підсвічено. Синтез мовлення з тексту відбуватимуться за допомогою засобів озвучення тексту операційної системи. Змінити голосовий рушій можна натисканням піктограми шестерні на панелі, яку буде показано програмою у режимі Читати уголос.

Ви також можете наказати програмі прочитати уголос позначений фрагмент тексту — додайте кнопку Читати уголос на панель вибору у налаштуваннях засобу перегляду, розділ Поведінка при позначенні.

Примітка

Підтримка озвучення тексту у браузерах є дуже неповною і сповненою вад, тому якість роботи інструмента Читати уголос у режимі перегляду у браузері залежить від якості підтримки озвучення тексту у браузері. Зокрема, у браузерах не працює підсвічування поточного слова, а зміна швидкості озвучення або голосового рушія призводить до того, що озвучення тексту розпочинається з початку.

Примітка

У Linux для роботи інструмента Читати уголос слід встановити і належно налаштувати Speech Dispatcher [https://freebsoft.org/speechd].

Примітка

У Windows підсистема SAPI, яка використовується для озвучення тексту, може бачити не усі встановлені голосові рушії. Можете скористатися настановами щодо уможливлення доступу до усіх голосових рушіїв [https://www.mobileread.com/forums/showpost.php?p=4084051&postcount=108].

Пошук тексту

У засобі перегляду передбачено дуже потужні можливості пошуку. Натисніть комбінацію клавіш Ctrl+F для отримання доступу до засобів керування переглядом. Потім натисніть кнопку пошуку. Найпростішою формою пошуку є простий пошук будь-якого введеного вами до поля пошуку тексту. Форму пошуку можна вибрати за допомогою поля режиму пошуку під полем для введення критерію пошуку. Доступні режими:

1. Містить — найпростіший типовий режим. Програма шукатиме текст, який введено до поля пошуку, усюди. Усі символи пунктуації, акценти і пробіли буде проігноровано. Наприклад, за запитом Pena буде знайдено penal, pen a, pen.a і Peña. Якщо ви позначите пункт З врахуванням регістру, акценти, пробіли і пунктуацію буде враховано.

2. Цілі слова — шукати цілі слова. Наприклад, у результаті пошуку pena буде знайдено слово Peña, але не слово Penal. Як і з випадком пошуку Містить вище, акценти і пунктуацію буде проігноровано, якщо не позначено пункт З врахуванням регістру.

3. Сусідні слова — шукати цілі слова, які розташовано поруч. Отже, наприклад, у результаті пошуку calibre чудовий буде знайдено місця, де слова``calibre`` і чудовий трапляються на відстані до шестидесяти символів одне від одного. Щоб змінити кількість символів, додайте число наприкінці списку слів. Наприклад, у результаті пошуку calibre чудовий різнокольоровий 120 буде знайдено місця, де ці три слова розділяє одне від одного не більше 120 символів. Зауважте, що при пошуку символи пунктуації та акценти не буде проігноровано.

4. Формальний вираз — вважати текст для пошуку формальним виразом. Щоб дізнатися більше про формальні вирази, ознайомтеся із відповідним розділом.

Перехід за посиланнями лише за допомогою клавіатури

У засобі для читання електронних книг передбачено Режим підказок, у якому ви можете клацати на посиланнях у тексті без використання миші. Натисніть комбінацію клавіш Alt+F, і програма підсвітить усі посилання у поточній області перегляду, показавши для кожного з них цифру або літеру. Натисніть клавішу літери, щоб перейти за відповідним посиланням. Натисканням клавіші Esc можна вийти з режиму підказок без переходу за посиланням.

Якщо в області перегляду виявиться понад 35 посилань, деяким з них буде призначено багатолітерні комбінації. У цьому випадку натисніть першу і другу клавіш комбінації, а потім натисніть клавішу Enter, щоб перейти за посиланням. Ви також можете скористатися натисканням клавіші Backspace, щоб скасувати помилку під час набирання комбінації літер.

Налаштовування вигляду і поведінки програми під час читання

Змінити розмір символів шрифту «на льоту» можна за допомогою пункту Розмір шрифту на панелі засобів керування переглядом, або натискання комбінацій клавіш Ctrl++ чи Ctrl+-, або утримуванням натиснутою клавіші Ctrl із одночасним прокручуванням коліщатка миші.

Кольори можна змінити за допомогою пункту Кольори у налаштуваннях засобу перегляду книг.

Ви можете змінити кількість показаних на екрані сторінок, а також розміри полів сторінки за допомогою розділу Компонування сторінки у налаштуваннях засобу перегляду.

Ви можете наказати програмі показувати нетипові верхні і нижні колонтитули, зокрема час до завершення читання книги, назву поточної глави, позицію у книзі тощо, за допомогою розділу Верхні і нижні колонтитули налаштувань засобу читання.

Доступ до ширшого спектра налаштувань можна отримати за допомогою розділу Стилі. За допомогою цього розділу ви можете вказати зображення тла, яке буде показано під текстом, а також таблицю стилів програма застосовуватиме до усіх книг, які ви у ній відкриватимете. За її допомогою ви зможете змінити стилі абзаців, вирівнювання тексту тощо. Приклади нетипових таблиць стилів, які використовуються користувачами calibre, можна знайти на форумі програми [https://www.mobileread.com/forums/showthread.php?t=51500].

Пошук у словнику

Переглядати значення слів у поточній книзі можна подвійним клацанням або довгим натисканням на слові, пошук значення якого слід виконати, із наступним натисканням кнопки пошуку, яка виглядає як бібліотека.

Копіювання тексту та малюнків

Ви можете вибрати текст та малюнки, позначивши фрагмент за допомогою миші, потім клацнувши правою кнопкою миші і вибравши пункт Копіювати для копіювання даних до буфера обміну. Скопійований матеріал може бути вставлений у іншу програму як неформатований текст та малюнки.

Збільшення масштабу зображень

Ви можете наказати програмі збільшити масштаб для показу зображення у повному розмірі в окремому вікні або подвійним клацанням, або довгим притисканням (на сенсорних пристроях) зображення. Крім того, ви можете клацнути на зображенні правою кнопкою миші і вибрати Переглянути зображення.

Клавіатурні скорочення

У засобі перегляду передбачено широкий набір клавіатурних скорочень, як і у решті calibre. Налаштувати скорочення можна на сторінці Налаштування засобу перегляду. Нижче наведено типові скорочення:

Клавіатурні скорочення засобу перегляду calibre

Клавіатурні скорочення

Дія

Home, Ctrl+↑, Ctrl+←

Гортати до початку поточного файла у книзі, яка складається з декількох файлів

Ctrl+Home

Гортати до початку книги

Ctrl+End

Гортати до кінця книги

End, Ctrl+↓, Ctrl+→

Гортати до кінця поточного файла у книзі, яка складається з декількох файлів

↑

Плавно гортати назад, неперервно у потоковому режимі і екранами у режимі сторінок

↓

Плавно гортати вперед, неперервно у потоковому режимі і екранами у режимі сторінок

←

Плавно гортати ліворуч потрохи у потоковому режимі і екранами у режимі сторінок

→

Плавно гортати праворуч потрохи у потоковому режимі і екранами у режимі сторінок

PageUp, Shift+Пробіл

Гортати назад екранами

PageDown, Пробіл

Гортати вперед екранами

Ctrl+PageUp

Гортати до попереднього розділу

Ctrl+PageDown

Гортати до наступного розділу

Alt+←

Назад

Alt+→

Вперед

Ctrl+T

Увімкнути або вимкнути показ панелі вмісту

Ctrl+S

Прочитати уголос

Alt+P

Швидко змініть параметри створенням та перемиканням на профілях

Alt+f

Переходити за посиланнями за допомогою клавіатури

Ctrl+C

Скопіювати до буфера

Alt+C

Скопіювати поточну адресу до буфера обміну

Ctrl+Shift+C

Скопіювати поточну адресу як адресу calibre:// до буфера обміну

/, Ctrl+f, Cmd+f

Почати пошук

F3, Enter

Знайти наступне

Shift+F3, Shift+Enter

Знайти попереднє

Ctrl+Плюс, Meta+Плюс

Збільшити розмір шрифту

Ctrl+Мінус, Meta+Мінус

Зменшити розмір шрифту

Ctrl+0

Відновити типовий розмір шрифту

Ctrl+]

Збільшити кількість сторінок на екрані

Ctrl+[

Зменшити кількість сторінок на екрані

Ctrl+Alt+C

Визначати кількість сторінок на екрані автоматично

F11, Ctrl+Shift+F

Увімкнути/Вимкнути повноекранний режим

Ctrl+M

Перемкнутися між режимом сторінок і неперервним режимом у компонуванні тексту

Ctrl+W

Увімкнути або вимкнути смужку гортання

Ctrl+X

Увімкнути або вимкнути режим посилань

Ctrl+B

Показати або приховати закладки

Ctrl+Alt+B

Нова закладка

Ctrl+N, Ctrl+E

Показати метадані книги

Ctrl+Alt+F5, Ctrl+Alt+R

Перезавантажити книгу

Ctrl+Shift+→

Після поточного позначеного вперед на одне слово

Ctrl+Shift+←

Після поточного позначеного назад на одне слово

Shift+→

Змінити поточне позначення вперед на символ

Shift+←

Змінити поточне позначення назад на символ

Shift+↓

Змінити поточне позначення вперед на рядок

Shift+Home

Розширити поточний позначений фрагмент до початку рядка

Shift+End

Розширити поточний позначений фрагмент до кінця рядка

Ctrl+A

Вибрати все

Shift+↑

Змінити поточне позначення назад на рядок

Ctrl+Shift+↓

Змінити поточне позначення вперед на абзац

Ctrl+Shift+↑

Змінити поточне позначення назад на абзац

Esc, Меню

Показати засоби керування переглядачем електронних книг

Ctrl+кома, Ctrl+Esc, Meta+Esc, Meta+кома

Показати вікно налаштувань засобу перегляду електронних книг

Ctrl+G, ;, :

Перейти до вказаного місця або позиції у книзі

Ctrl+Пробіл

Увімкнути або вимкнути автогортання

Alt+↑

Швидше автогортання

Alt+↓

Повільніше автогортання

Ctrl+I

Показати або приховати панель Інспектора

Ctrl+L

Показати або приховати панель пошуку слів

Ctrl+Q (Cmd+Q у macOS)

Вийти

Ctrl+P

Надрукувати книгу до PDF

Ctrl+F11

Увімкнути/Вимкнути панель

Ctrl+H

Перемкнути панель підсвічування

Ctrl+D

Редагувати цю книгу

Непридатний до перенесення текст

У деяких книгах надто широкі фрагменти тексту без пробілів можуть вилізати за межі сторінок. Прикладами таких фрагментів є таблиці або вміст теґів <pre>. Щоб переглянути такі фрагменти, слід перемкнути засіб перегляду у «рухомий режим» натисканням комбінації клавіш Ctrl+M. Крім того, ви можете додати такий код CSS до розділу стилів налаштувань засобу перегляду з метою примусового перенесення рядків тексту всередині теґів <pre>:

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

Компонування вашої книги для роботи із переглядачем calibre

Перелядач calibre встановлює у кореневому елементі клас is-calibre-viewer. Отже, ви можете писати правила CSS, які стосуватимуться лише переглядача. Крім того, переглядач встановлює такі класи у елементі body:

body.calibre-viewer-dark-colors

Встановлюють при використанні темної схеми кольорів

body.calibre-viewer-light-colors

Встановлюють при використанні світлої схеми кольорів

body.calibre-viewer-paginated

Встановлюють у режимі поділу на сторінки

body.calibre-viewer-scrolling

Встановлюють у режимі сувою (без поділу на сторінки)

body.calibre-footnote-container

Встановіть при показі контекстної підрядкової примітки

Нарешті, ви можете скористатися схемою кольорів calibre за допомогою змінних CSS [https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties]. Переглядач calibre визначає такі змінні: --calibre-viewer-background-color, --calibre-viewer-foreground-color і, необов’язково, --calibre-viewer-link-color у темах кольорів, які визначають колір посилань.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Перетворення даних електронних книг

Перетворення даних електронних книг

Систему перетворення книг з одного формату на інший у calibre створено дуже простою у користуванні. Зазвичай, достатньо просто додати книгу до бібліотеки calibre, натиснути кнопку перетворення, і calibre зробить усе можливе, щоб книга у перетвореному форматі була якомога ближчою до тієї, яку ви передали для перетворення. Втім, calibre може працювати із дуже широким колом форматів вхідних даних, і не усі з цих форматів придатні для перетворення на електронні книги. Якщо ви маєте справу саме з таким незручним форматом або хочете отримати ширші можливості з керування процесом перетворення, у calibre передбачено багато параметрів, за допомогою яких ви зможете коригувати процес перетворення. Втім, слід зауважити, що система перетворення форматів книг у calibre не є повноцінною заміною справжнього редактора електронних книг. Для редагування книг ми рекомендуємо спочатку перетворити їх на книги у форматі EPUB або AZW3 за допомогою calibre, а вже потім скористатися вбудованим до програми редактором електронних книг, щоб надати їх довершеної форми. Після цього, ви зможете скористатися відредагованою книгою як вхідним форматом для перетворення даних у інші формати за допомогою calibre.

Цей документ, в основному, стосується параметрів перетворення, які можна знайти у діалоговому вікні перетворення, знімок якого наведено нижче. Доступ до усіх цих параметрів можна також отримати за допомогою інтерфейсу командного рядка для перетворення. Опис цього інтерфейсу можна знайти у розділі ebook-convert. У calibre ви можете отримати довідку щодо окремого параметра, навівши вказівник миші на відповідну частину інтерфейсу програми і трохи почекавши. Має з’явитися панель підказки із описом параметра.

Зміст

- Вступ

- Вигляд та поведінка

- Налаштування сторінки

- Евристична обробка

- Пошук і заміна

- Виявлення структури

- Зміст

- Використання зображень як заголовків глав під час перетворення вхідних документів HTML

- Використання атрибутів теґів для текстових записів у таблиці змісту

- Як встановлюються та зберігаються параметри для перетворення

- Підказки для окремих форматів

Вступ

Перше, що слід зрозуміти про систему перетворення, — це те, що вона нагадує конвеєр. Схематично, вона виглядає так:

Вхідні дані спочатку перетворюються на XHTML за допомогою відповідного додатка вхідних даних. Отриманий код HTML далі перетворюється. На останньому кроці, оброблений код XHTML трансформується відповідно до правил вказаного формату виведення за допомогою відповідного додатка виведення. Якість результатів перетворення може бути зовсім різною — усе залежить від формату вхідних даних. Дані у деяких форматах перетворити набагато простіше ніж у інших. Список найкращих початкових форматів даних можна знайти тут.

Основна робота виконується під час перетворень виведених даних XHTML. Передбачено різноманітні перетворення, наприклад, для вставлення метаданих книги як сторінки на початку книги, для виявлення заголовків глав і автоматичного створення списку змісту, для пропорційного коригування розмірів шрифтів тощо. Важливо пам’ятати, що усі перетворення виконуються із даними XHTML, які є результатом роботи додатка вхідних даних, а не над вмістом початкового файла. Отже, наприклад, якщо ви накажете calibre перетворити файл RTF на EPUB, програма спочатку на внутрішньому рівні перетворить його на XHTML, виконає різноманітні перетворення коду XHTML, а потім вже додаток виведення створить файл EPUB, автоматично запише усі метадані, список змісту тощо.

Ви можете переглянути поступ обробки за допомогою параметра діагностики . Достатньо просто вказати шлях до теки із діагностичними даними. Під час перетворення calibre зберігатиме XHTML, створені на різних етапах конвеєра перетворення, у різних підтеках. Цими чотирма підтеками є:

Кроки конвеєра перетворення

Тека

Опис

input

Цей каталог містить результати роботи додатка вхідних даних у форматі HTML. Використовується для діагностики додатка вхідних даних.

parsed

Результат попередньої обробки і перетворення на XHTML результатів роботи додатка вхідних даних. Використовується для діагностики роботи засобу визначення структури.

structure

Обробка після визначення структури, але перед спрощенням CSS та перетворенням розмірів шрифтів. Використовується для діагностики перетворення розмірів шрифтів та CSS.

processed

Дані перед передаванням електронної книги додатку виведення даних. Використовується для діагностики додатка виведення даних.

Якщо ви хочете трохи змінити вхідний документ перед перетворенням даних за допомогою calibre, найкращим шляхом буде редагування файлів у підтеці input із наступним стисканням їх у форматі ZIP. Далі, файлом ZIP слід скористатися як вхідним форматом для наступного перетворення. Для цього скористайтеся діалоговим вікном Редагувати метаінформацію для додавання файла zip як формату книги, а потім у верхньому лівому куті діалогового вікна перетворення виберіть ZIP як формат вхідних даних.

У цьому документі, в основному, описано різноманітні перетворення, які виконуються над проміжним XHTML, і те, як керувати ними. Наприкінці наведено деякі підказки щодо кожного з форматів вхідних і вихідних даних.

Вигляд та поведінка

Зміст

- Шрифти

- Текст

- Форматування

- Стиль

- Перетворити стилі

- Перетворення HTML

Ця група параметрів керує різноманітними аспектами вигляду перетвореної електронної книги.

Шрифти

Однією з найприємніших можливостей у читанні електронних книг є можливість простого коригування розмірів шрифтів для пристосування до індивідуальних потреб та умов освітлення. У calibre використано продумані алгоритми для забезпечення однорідності вигляду усіх книг, незалежно від того, які розміри шрифтів визначено у початковому документі.

Основним розміром шрифту документа є найуживаніший розмір шрифту, у цьому документі, тобто розмір шрифту в основному тексті документа. Коли ви вказуєте Основний розмір шрифту, calibre автоматично пропорційно змінює розмір усіх шрифтів у документі, отже цей найуживаніший розмір шрифту дорівнюватиме вказаному вами значенню, а інші розміри шрифтів буде скориговано відповідно до цього розміру. Вибравши більше значення основного розміру шрифту ви можете збільшити розміри усіх шрифтів у документі, і навпаки. Щоб отримати кращі результати, під час встановлення основного розміру шрифту вам слід також вказати ключ розмірів шрифту.

Зазвичай, автоматично вибирає розмір базового шрифту calibre відповідно до вибраного вами профілю виведення даних (див. Налаштування сторінки). Втім, ви можете перевизначити цей розмір тут, якщо типовий розмір вас не влаштовує.

За допомогою пункту Ключ розмірів шрифту ви можете керувати масштабуванням неосновних розмірів шрифту. Алгоритм масштабування шрифтів працює на основі ключа розмірів шрифту, який є простим списком розмірів, відокремлених комами. Ключ розмірів шрифту повідомляє calibre, на скільки «кроків» більшим або меншим має бути заданий розмір шрифту порівняно із основним розміром шрифту. Ідея полягає у тому, що у документі має бути обмежена кількість розмірів шрифту. Наприклад, один розмір для основного тексту, двійко розмірів для різних рівнів заголовків і двійко розмірів для верхніх і нижніх індексів, а також приміток. Ключ розміру шрифту надає calibre змогу поділити розміри шрифтів у вхідних документах на окремі «передачі», відповідно до різних логічних частин документа.

Проілюструємо викладене вище прикладом. Припустімо, що початковий документ, який ми перетворюємо, було створено кимось із ідеальним зором — у ньому вказано розмір шрифту у 8 пунктів. Це означає, що масив тексту у документі має розмір символів у 8 пунктів, а розмір символів у заголовка є дещо більшим (скажімо, 10 або 12 пунктів), а розмір підрядкових приміток є дещо меншим, скажімо, 6 пунктів. Тепер, якщо ми скористаємося такими параметрами:

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

У виведеному документі основним шрифтом буде 12 пунктів, заголовки — 14 і 16 пунктів, а підрядкові примітки — 8 пунктів. Тепер припустімо, що нам потрібно зробити найбільші заголовки ще помітнішими і збільшити розмір шрифту підрядкових приміток. Щоб цього досягти, параметри шрифту слід змінити так:

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

Найбільші заголовки тепер буде показано шрифтом розміром у 18 пунктів, а підрядкові примітки — шрифтом розміром у 9 пунктів. Ви можете побавитися з цими параметрами, щоб визначити оптимальний для вас розмір, скориставшись майстром масштабування шрифтів, доступ до якого можна отримати за допомогою натискання маленької кнопки, розташованої поруч із пунктом Ключ розміру шрифту.

Тут також можна вимкнути будь-яку зміну розмірів шрифтів під час перетворення, якщо ви хочете зберегти розміри шрифтів початкового документа.

Пов’язаним параметром є Висота рядка. Параметр висоти рядка керує вертикальною висотою рядків. Типово, (значення висоти рядка 0) ніяких дій із висотою рядків не виконується. Якщо ви вкажете нетипове значення, висоту рядків буде змінено всюди, де її не вказано іншим чином. Втім, це доволі грубий інструмент, ним не слід зловживати. Якщо ви хочете змінити висоту рядків у якомусь розділі вхідних даних, краще скористатися інструментом Додаткові CSS.

За допомогою цього розділу ви можете також наказати calibre вбудувати усі використані шрифти до книги. Це надасть змогу користуватися шрифтами на пристроях для читання, на яких ці шрифти не встановлено.

Текст

Текст може бути вирівняним чи невирівняним. У вирівняному за шириною тексті будуть додаткові пробіли між словами для утворення рівномірного правого краю. Декому подобається вирівняний за шириною текст, декому — ні. Зазвичай, calibre зберігає вирівнювання початкового документа. Якщо ви хочете змінити параметри вирівнювання, скористайтеся пунктом Вирівнювання тексту у цьому розділі.

Ви також можете наказати calibre Обробити пунктуацію, тобто замінити звичайні лапки, дефіси та трикрапки на типографічно коректні варіанти. Зауважте, що алгоритм заміни не є ідеальним, отже, варто переглянути результати. Можна також скористатися і зворотною дією, Скасувати обробку пунктуації.

Нарешті, передбачено пункт Кодування вхідних даних. У застарілих форматах документів іноді не вказується кодування символів. Перетворення таких документів призводить до того, що кириличні символи та лапки перекодовуються із помилками. calibre намагається автоматично визначити кодування символів початкового документа, але робить це не завжди успішно. Типовим кодуванням документів, створених у Windows, є cp1251. Вам також слід ознайомитися із розділом Як перетворити файл, що містить нелатинські символи або теґи для позначення лапок?, щоб дізнатися більше про проблеми із кодуванням.

Форматування

Зазвичай, абзаци у XHTML позначаються порожнім рядком між ними без початкового відступу. У calibre передбачено декілька параметрів, які призначено для того, щоб змінити таку типову поведінку. Використання параметра Вилучати інтервали між абзацами призведе до примусового вилучення інтервалів між абзацами. Також буде встановлено відступ першого рядка тексту у 1.5em (можна змінити) для позначення абзаців. Параметр Додати порожній рядок між абзацами виконує прямо протилежну дію — забезпечує наявність точного одного порожнього рядка між усіма абзацами. Дія обох параметрів очевидна: вилучення інтервалу або його додавання до усіх абзаців (з технічної точки зору, до усіх теґів <p> і <div> у коді). Так зроблено для того, щоб ви могли просто позначити відповідний пункт і отримати потрібний результат, незалежно від того, наскільки поганим є коду у файлі вхідних даних. Єдиним виключенням є вхідні дані, у яких для реалізації інтервалів між абзацами використано жорсткі розриви рядків.

Якщо ви хочете вилучити інтервали між абзацами, окрім декількох вибраних, не використовуйте ці пункти. Замість цього, додайте такий код CSS до Додаткові CSS:

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

Далі, у вашому початковому документі позначте абзаци, які слід відокремити інтервалами за допомогою class=»spacious». Якщо початковий документ не є документом HTML, скористайтеся параметром діагностики, описаним у «Вступі», щоб отримати HTML (скористайтеся підтекою input).

Ще одним корисним пунктом є пункт Лінеаризувати таблиці. У деяких документах із поганим компонуванням для керування показом тексту на сторінці використовують таблиці. Перетворення таких документів призводить до виходу тексту за межі сторінки та інших викривлень компонування. За допомогою цього параметра можна наказати програмі видобути вміст з таблиць і представити його у лінійному форматі. Зауважте, що використання цього пункту призведе до лінеаризації усіх таблиць. Тому ним слід користуватися, лише якщо ви впевнені, що у початковому документі таблиці не використовуються там, де це справді потрібно, наприклад для показу табличних даних.

Стиль

За допомогою пункту Додатковий CSS ви можете вказати довільну таблицю стилів CSS, яку буде застосовано до усіх файлів HTML у вхідних даних. Цю таблицю стилів CSS буде застосовано із дуже високим пріоритетом, вона має перевизначити усе, що визначено у більшості таблиць CSS вхідного документа. Цим пунктом ви можете скористатися, наприклад, для користування представлення або компонування документа. Наприклад, якщо ви хочете, щоб усіх абзаци класу endnote було вирівняно праворуч, просто додайте такий код:

.endnote { text-align: right }

або, якщо ви хочете змінити відступи у всіх абзацах:

p { text-indent: 5mm; }

Параметр Додаткове CSS є дуже потужним, але для того, щоб скористатися ним повністю, потрібне розуміння того, як працює CSS. Ви можете скористатися пунктом конвеєра діагностики, описаним вище, щоб переглянути CSS, які є у початковому документі.

Простішим варіантом є використання пункту Фільтрувати інформацію щодо стилів. За його допомогою ви можете вилучити усі властивості CSS вказаних типів з документа. Наприклад, ви можете вилучити усе розфарбовування шрифтів.

Перетворити стилі

Це найпотужніша можливість зі зміни стилів. Ви можете скористатися нею для визначення правил зміни стилів на основі різноманітних умов. Наприклад, ви можете скористатися нею, щоб змінити зелений колір на синій або вилучити усі позначення шрифту напівжирним, або розфарбувати усі заголовки у певний колір тощо.

Перетворення HTML

Подібне до перетворення стилів, але надає вам змогу вносити зміни до коду HTML книги. Ви можете замінити один теґ іншим, додати класи або інші атрибути до теґів на основі їхнього вмісту тощо.

Налаштування сторінки

Параметри налаштовування сторінки керують компонуванням сторінки, зокрема полями та розмірами екрана. Передбачено параметри визначення полів сторінки, які буде використано додатком виведення даних, якщо у вибраному форматі виведення передбачено підтримку полів сторінки. Крім того, вам слід вибрати формат вхідних і вихідних даних. Обидва набори профілів, в основному, визначають спосіб обробки вимірів у вхідних або вихідних документа, розміри екрана та типові параметри масштабування шрифтів.

Якщо ви впевнені, що файл, який ви перетворюєте, було призначено для читання лише на певному пристрої із використанням певного програмного забезпечення, виберіть відповідний профіль вхідних даних. Якщо ви хочете створити файли, які призначено для певного типу пристроїв, виберіть відповідний профіль виведення. В інших випадках виберіть один із загальних профілів виведення. Якщо ви перетворюєте дані до формату MOBI або AZW3, вам майже безумовно слід вибрати один із профілів виведення на пристрої Kindle. В інших випадках для сучасних пристроїв для читання електронних книг варто вибрати профіль виведення Типове ел. чорнило, дуже велика.

Профіль виведення також визначає розмір екрана. Тобто, наприклад, розміри усіх зображень буде автоматично змінено так, щоб вони вміщалися на екрані у певних форматах виведення даних. Отже, вам слід вибрати профіль пристрою, екран якого подібний до екрана вашого пристрою для читання електронних книг.

Евристична обробка

У засобі евристичної обробки передбачено широкий спектр функцій, якими можна скористатися для виявлення і виправлення типових проблем, пов’язаних із помилковим форматуванням вхідних документів. Скористайтеся цими функціями, якщо маєте проблеми із форматуванням у вхідному документі. Оскільки при роботі ці функції покладаються на типові взірці, не забувайте, що іноді певні параметри можуть призвести до погіршення результатів, отже, будьте обережні. Наприклад, використання деяких параметрів призводить до вилучення усіх замінників нерозривних пробілів або може призводити до спрацьовування функції, коли у цьому немає потреби.

Увімкнути евристичну обробку

За допомогою цього пункту можна задіяти крок Евристична обробка у конвеєрі перетворення. Для застосування різноманітних підфункцій слід позначити цей пункт.

З’єднати рядки

Позначення цього пункту призводить до того, що calibre намагається виявити і виправити жорсткі перенесення рядків у документі за допомогою аналізу пунктуації та довжини рядків. Спочатку calibre спробує виявити, чи існують жорсткі розриви рядків. Якщо ознак таких розривів виявлено не буде, calibre не намагатиметься з’єднати розірвані рядки. Коефіцієнт з’єднування рядків можна зменшити, якщо ви хочете «примусити» calibre з’єднувати рядки.

Коефіцієнт розбирання рядків

Цей параметр призначено для керування алгоритмом, який calibre використовує для усування жорстких розривів рядків. Наприклад, якщо значенням параметра є 0.4, calibre з’єднуватиме рядки з кінця рядків, чия довжина є меншою за 40% довжини усіх рядків документа. Якщо у вашому документі виправлення потребують лише декілька рядків, слід зменшити значення цього параметра до числа від 0.1 до 0.2.

Виявити і розмітити неформатовані заголовки глав та розділів

Якщо у документі немає заголовків глав і заголовки оформлено якось інакше від решти тексту, calibre може скористатися цим пунктом для того, щоб спробувати виявити заголовки і використати для них теґи заголовків. Для заголовків глав використовуються теґи <h2>; теґи <h3> використовуються для усіх інших виявлених заголовків.

Ця функція не створюватиме списку вмісту, але у багатьох випадках допоможе типовому засобу виявлення глав calibre правильно виявити глави і побудувати список змісту. Скоригуйте вираз XPath у розділі «Виявлення структури», якщо список змісту не буде створено автоматично. Якщо у документі не використано інших заголовків, встановлення значення «//h:h2» у полі виявлення структури є найпростішими способом створення списку вмісту для документа.

Вставлені заголовки не буде форматовано. Для застосування форматування скористайтеся пунктом Додаткове CSS у параметра перетворення «Зовнішній вигляд». Наприклад, щоб мітки заголовків розташовувалися у центрі, скористайтеся таким кодом:

h2, h3 { text-align: center }

Змінювати нумерацію у послідовностях теґів <h1> і <h2>

Деякі видавці форматують заголовки глав за допомогою декількох послідовних теґів <h1> або <h2>. Використання типових параметрів перетворення calibre призведе до поділу таких заголовків на частини. За допомогою цього пункту можна перенумерувати теґи заголовків для запобігання поділу.

Вилучити порожні рядки між абзацами

За допомогою цього пункту можна наказати calibre аналізувати порожні рядки, включені до документа. Якщо усі абзаци відокремлено один від одного порожніми рядками, calibre вилучить усі такі порожні абзаци. Послідовність з декількох порожніх рядків буде вважатися розривом сцени і зберігатиметься як окремий абзац. Цей пункт відрізняється від пункту Вилучати інтервали між абзацами на вкладці Зовнішній вигляд тим, що його використання насправді змінює вміст HTML, а не стилі документа. Цей пункт може також бути використано для вилучення абзаців, які вставлено за допомогою пункту Вставити порожній рядок.

Забезпечити належне форматування розривів сцен

Якщо буде позначено цей пункт, calibre спробує виявити типові позначки поділу сцен і вирівняє їх за центром сторінки. До «м’яких» позначок поділу сцен, тобто поділу сцен додатковими пробілами, буде застосовано стиль для забезпечення того, щоб їх не було показано у поєднанні із розривами сторінок.

Замінити м’які розриви сцен

Якщо буде позначено цей пункт, calibre замінюватиме знайдені програмою позначки розриву сцен на вказаний користувачем текст-замінник. Будь ласка, зауважте, що не усі символи-роздільники може бути правильного показано на усіх пристроях для читання книг.

Загалом, вам слід уникати використання теґів HTML. Calibre відкине усі теґи і використовуватиме попередньо визначену розмітку. Теґи <hr />, тобто горизонтальні лінії, та теґи <img> є виключеннями з цього правила. Горизонтальні лінії може бути додаткового вказано за допомогою стилів. Якщо ви додаватимете власний стиль, не забудьте включити параметр «width», інакше дані стилю буде відкинуто. Теґами зображень можна користуватися, але calibre не надає можливостей із додавання зображень під час перетворення. Зображення має бути додано після перетворення за допомогою інструмента «Редагувати книгу».

Приклад теґу зображення (зображення розташовуються у теці «Images» у EPUB після перетворення):

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

Приклад горизонтальної лінії зі стилями:

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

Вилучати непотрібні дефіси

Якщо буде позначено цей пункт, calibre виконає аналіз усього тексту з переносами у документі. Для аналізу буде використано сам текст документу. Це надає змогу calibre акуратно вилучити переноси у будь-яких документах будь-якою мовою, навіть у складених словах та наукових термінах. Основним недоліком цієї методики є те, що переноси у словах, які трапляються у документі лише один раз, не вилучатимуться. Аналіз виконується у два проходи. Під час першого проходу програма аналізує кінці рядків. Рядки об’єднуватимуться, лише якщо у документі є таке саме слово без переносу або з дефісом. Під час другого проходу програма аналізує усі слова з дефісами у документі і вилучає дефіси, якщо у іншій частині документа є те саме слово без дефіса.

IПозначати курсивом загальні слова та взірці

Якщо позначено, calibre шукатиме загальні слова і взірці, які позначають курсив і форматуватиме курсивом відповідні частини тексту. Прикладами є загальні правила позначення курсивного тексту, зокрема ~слово~ або фрази, які зазвичай друкують курсивом, наприклад латинські вставки «etc.» або «et cetera».

Замінити відступи за допомогою об’єктів відступами CSS

У деяких документах використовуються певні правила щодо визначення відступів у тексті за допомогою об’єктів-замінників нерозривних пробілів. Якщо позначено цей пункт, calibre намагатиметься виявити такого роду форматування і перетворити його на відступи у 3% ширини тексту за допомогою CSS.

Пошук і заміна

Ці параметри корисні, в основному, для перетворення документів PDF або результатів оптичного розпізнавання тексту, хоча ними можна також скористатися і для виправлення документів із відповідними проблемами. Наприклад, під час певних перетворень у тексті можуть залишитися верхні і нижні колонтитули сторінок. Для перетворень використовуються формальні вирази для пошуку і виявлення верхніх і нижніх колонтитулів та інших довільних фрагментів тексту з наступним їх вилученням або заміною. Не забувайте, що обробці підлягатиме проміжний документ XHTML, створений конвеєром перетворення. Передбачено майстер, який допоможе вам створити формальний вираз для документа. Натисніть кнопку із чарівною паличкою, розташовану поруч із полем для введення формального виразу, і натисніть кнопку Тест після створення вашого формального виразу. Відповідники у тексті буде підсвічено жовтим кольором.

Пошук працює на основі формального виразу Python. Увесь знайдений текст буде просто вилучено з документа або замінено за допомогою взірця. Взірець для заміни є необов’язковим, якщо його не вказано, знайдений за взірцем текст буде вилучено з документа. Дізнатися більше про формальні вирази та їхній синтаксис можна з розділу Все про використання формальних виразів в calibre.

Виявлення структури

Використовуючи засоби виявлення структури, calibre намагається вичленити структурні елементи у вхідному документі, якщо ці елементи не було належним чином вказано. Прикладами таких структурних елементів є заголовки глав, розриви сторінок, верхні і нижні колонтитули. Як ви могли здогадатися, процедура виявлення залежить від початкового стану тексту. На щастя, у calibre передбачено дуже потужні можливості для керування нею. Ця потужність, звичайно, ускладнює налаштовування, але якщо ви присвятите трохи часу вивченню цих можливостей, ваші знання заощадять вам дуже багато часу.

Поділ на глави і сторінки

У calibre є два набори параметрів для виявлення глав і вставляння розривів сторінок. Це іноді може збити з пантелику, оскільки типово calibre вставлятиме розриви сторінок перед виявленими главами, а також у місцях, які відповідатимуть параметрам розриву сторінок. Причиною цього є те, що часто місця, куди слід вставити розрив сторінки, не перебувають на межі глав. Крім того, виявлені глави може бути вставлено до автоматично створеного списку змісту.

У calibre використовується XPath, потужна мова, яка надає змогу користувачеві вказувати місця меж глав та розривів сторінок. Можливо, спочатку XPath видасться вам трохи складною у користуванні. На щастя, у нас є підручник з XPath, який є частиною підручника для користувачів програми. Не забувайте, що засоби виявлення структури працюють із проміжним кодом XHTML, який створено під час обробки у конвеєрі перетворення. Скористайтеся параметром діагностики, описаним у розділі Вступ, щоб визначити відповідні параметри для вашої книги. Крім того, передбачено кнопку майстра створення виразів XPath, яка може допомогти вам зі створенням простих виразів XPath.

Типово, для виявлення глав у calibre використовується такий вираз:

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

Цей вираз є доволі складним, оскільки у ньому зроблено спробу одночасно обробити декілька типових випадків. Це означає, що calibre припускатиме, що глави починаються з теґів «<h1>» або «<h2>», які містять якесь із контрольних слів (chapter, book, section або part) або атрибут class=»chapter».

Пов’язаним пунктом налаштувань є пункт Позначка глави. За допомогою цього пункту ви можете визначити дію, яку має виконати calibre, коли виявить главу. Типово, програма вставляє розрив сторінки перед главою. Ви можете наказати програмі вставити горизонтальну лінію замість розриву або доповнити розрив горизонтальною лінією. Також можна наказати програмі не виконувати ніякого додаткового форматування тексту.

Типовими параметрами для виявлення розривів сторінок є:

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

що означає, що calibre типово вставлятиме розриви сторінок перед кожним теґом <h1> і <h2>.

Примітка

Типові вирази можуть змінюватися, залежно від формату вхідних даних, які ви перетворюєте.

Різне

У цьому розділі є ще декілька пунктів.

Вставити метадані як сторінку на початку книги

Однією із великих переваг calibre є те, що програма надає вам змогу зберігати повний спектр метаданих щодо усіх ваших книг. Зберігаються, наприклад, оцінка, мітки, коментарі тощо. За допомогою цього пункту можна створити окрему сторінку із усіма метаданими і вставити її до перетвореної електронної книги, типово, одразу після обкладинки. Можна вважати цю сторінку способом створення вашої нетипової суперобкладинки.

Вилучити перше зображення

Іноді у початкових документах, які ви перетворюєте, обкладинку включать до самої книги, а не використовують окрему сторінку обкладинки. Якщо ви вкажете ще одну обкладинку у calibre, у перетвореній книзі буде дві обкладинки. За допомогою цього пункту можна вилучити перше зображення з початкового документа, таким чином забезпечивши існування у перетвореній книзі лише однієї обкладинки, тієї яку визначено у calibre.

Зміст

Якщо у вхідному документі список вмісту є частиною метаданих, calibre просто ним скористається. Втім, у багатьох старих форматах електронних книг або не передбачено підтримки списку змісту у метаданих, або у окремих документів немає такого списку змісту. У таки випадках пункти з цього розділу можуть допомогти вам в автоматичному створенні списку змісту у перетвореній електронній книзі на основі вмісту вхідного документа.

Примітка

Отримати ідеальний результат за допомогою цих пунктів іноді доволі непросто. Якщо ви надаєте перевагу створенню або редагуванню списку змісту вручну, виконайте перетворення даних у формат EPUB або AZW3 і позначте той пункт у нижній частині розділу «Зміст» діалогового вікна перетворення, який має мітку Після завершення перетворення виконати коригування змісту вручну. У результаті наприкінці процедури перетворення програма запустить інструмент редагування змісту. За його допомогою ви зможете створити записи у списку змісту простим клацанням у тому місці книги, куди має вказувати відповідний запис списку. Крім того, ви можете скористатися редактором змісту окремо, без виконання перетворення. Відкрийте сторінку Налаштування  →  Інтерфейс  →  Панелі інструментів і додайте кнопку редактора змісту на головну панель інструментів вікна програми. Далі, просто позначте пункт книги, яку хочете редагувати, і натисніть кнопку редактора змісту.

Першим пунктом є пункт Примусово використати автоматично створений зміст. Якщо ви позначите цей пункт, calibre перевизначить будь-який список змісту, який буде виявлено у метаданих вхідного документа, на автоматично створений програмою список змісту.

Типовим способом роботи засобу автоматичного створення списку змісту є такий спосіб: calibre спочатку намагається додати будь-які виявлені глави до створеного списку змісту. Дізнатися про те, як налаштувати виявлення глав, можна з розділу Виявлення структури наведеного вище. Якщо ви не хочете включати виявлені глави до створеного списку змісту, позначте пункт Не додавати виявлені глави.

Якщо буде виявлено меншу за вказану у пункті Поріг глав кількість глав, calibre додасть до списку змісту усі знайдені у документі гіперпосилання. Часто це дає добрий результат, оскільки багато початкових документів включають список змісту із гіперпосиланнями на початку. Керувати кількістю посилань можна за допомогою пункту Кількість посилань. Якщо встановити нульову кількість, посилання не додаватимуться. Якщо встановити додатну кількість посилань, до змісту буде додано не більше за вказану кількість посилань.

calibre автоматично усуває дублікати зі створеного списку змісту. Втім, якщо у списку все ж є додаткові небажані записи, ви можете усунути їх за допомогою пункту Фільтр змісту. Тут слід вказати формальний вираз, який відповідає заголовкам записів у створеному списку змісту. Якщо буде знайдено відповідники цього формального виразу, програма їх усуне. Наприклад, щоб усунути усі записи із заголовками «Next» або «Previous», скористайтеся таким формальним виразом:

Next|Previous

За допомогою пунктів Рівень 1,2,3 змісту ви можете створити багаторівневий список змісту. Значеннями у пунктах є вирази XPath, які відповідають теґами у проміжному документі XHTML, створеному конвеєром перетворення. Щоб дізнатися про те, як отримати доступ до цього документа XHTML, ознайомтеся із розділом Вступ. Також ознайомтеся із розділом Підручник з XPath, щоб дізнатися про те, як конструювати вирази XPath. Поряд із кожним пунктом є кнопка, натискання якої запускає майстер створення базових виразів XPath. Наведений нижче простий приклад ілюструє використання цих пунктів.

Припустімо, що ви маєте вхідний документ, який дає код XHTML, що виглядає так:

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

Отже, ми встановимо такі значення параметрів:

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

Це призведе до автоматичного створення такої дворівневої таблиці змісту:

Chapter 1
Section 1.1
Section 1.2
Chapter 2
Section 2.1

Попередження

Підтримку багаторівневих таблиць змісту передбачено не в усіх форматах виведення даних. Спочатку варто спробувати виведення до EPUB. Якщо дані буде виведено вдало, спробуйте вибрати інший бажаний для вас формат.

Використання зображень як заголовків глав під час перетворення вхідних документів HTML

Припустімо, ви хочете скористатися зображенням як заголовком глави, але хочете, щоб програма calibre змогла автоматично створити список змісту на основі заголовків глав. Скористайтеся таким кодом HTML, щоб досягти цього:

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

Вкажіть у полі Рівень 1 змісту значення //h:h2. Тоді, для другого розділу calibre візьме заголовок зі значення атрибута title теґу <h2>, оскільки у тезі немає тексту.

Використання атрибутів теґів для текстових записів у таблиці змісту

Якщо маєте доволі довгі заголовки глав і хочете скоротити їх для списку змісту, можете скористатися для цього атрибутом заголовка, ось так:

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

Вкажіть у полі Рівень 1 змісту значення //h:h2/@title. Тоді calibre візьме назву зі значення атрибута title у теґах <h2>, замість використання тексту у тезі. Зверніть увагу на кінцеве /@title у виразі XPath. Ви можете скористатися подібною формою запису для того, щоб повідомити calibre, що текст слід брати із будь-якого визначеного вами атрибута.

Як встановлюються та зберігаються параметри для перетворення

Передбачено два місця, у яких можна встановити параметри перетворення у calibre. Першим є сторінка Налаштування  →  Перетворення. Ці параметри є типовими для перетворення. Якщо ви спробуєте перетворити якусь нову книгу, встановлені за допомогою цієї сторінки параметри буде використано як типові.

Ви також можете змінити параметри перетворення у діалоговому вікні перетворення окремої книги. Під час перетворення книги calibre запам’ятовує використані для цієї книги значення параметрів, отже, якщо ви захочете перетворити книгу знову, збережені параметри матимуть пріоритет перед типовим набором параметрів, які визначено у вікні налаштувань програми. Ви можете відновити типові значення параметрів перетворення окремої книги за допомогою натискання кнопки Відновити типові у діалоговому вікні перетворення окремої книги. Ви можете вилучити збережені значення параметрів для групи книг: позначте усі книги групи, а потім натисніть кнопку редагування метаданих, щоб відкрити вікно пакетного редагування метаданих. У нижній частині цього вікна є пункт для вилучення збережених значень параметрів перетворення.

Якщо ви пакетно перетворюєте набір книг, параметри буде використано у такому порядку (останній має пріоритет):

- З типового набору Налаштування -> Перетворення

- Зі збережених параметрів перетворення для кожної з книг (якщо такі визначено). Використання цих параметрів може бути вимкнено за допомогою пункту у верхньому лівому куті діалогового вікна пакетного перетворення.

- З набору параметрів у діалоговому вікні пакетного перетворення

Зауважте, що остаточні параметри для кожної книги під час пакетного перетворення буде збережено і повторно використано для наступних перетворень книги. Оскільки найвищий пріоритет при пакетному перетворенні мають значення параметрів, які визначено у діалоговому вікні пакетного перетворення, ці значення параметрів перевизначать будь-які специфічні для книги значення параметрів. Отже, засіб пакетного перетворення слід використовувати лише для перетворення книг, які потребують подібних значень параметрів. Виключеннями є метадані і специфічні для вхідних форматів параметри. Оскільки у діалоговому вікні пакетного перетворення немає пунктів, які пов’язано із цими двома категоріями параметрів, значення цих параметрів буде запозичено зі списку специфічних для книги значень параметрів (якщо такі визначено) або типових значень параметрів.

Примітка

Переглянути параметри перетворення, які використовуватимуться для будь-якого перетворення, можна натисканням піктограми обертання у нижньому правому куті вікна з наступним подвійним клацанням на пункті окремого завдання з перетворення. У відповідь на ці дії програма відкриє журнал перетворення, де у верхній частині міститимуться дані щодо використаних параметрів.

Підказки для окремих форматів

У цьому розділі наведено підказки щодо специфіки перетворення певних форматів даних. Параметри, які є специфічними для певного формату, як вхідних, так і вихідних даних, доступні у діалоговому вікні перетворення у власних розділах. Наприклад, Вхідні дані TXT або Вивід в EPUB.

Перетворення документів Microsoft Word
calibre може автоматично перетворювати файли .docx, які створено у Microsoft Word 2007 та новіших версіях. Просто додайте файл у calibre і натисніть кнопку перетворення.

Примітка

Існує демонстраційний файл .docx [https://calibre-ebook.com/downloads/demos/demo.docx], за допомогою якого можна ознайомитися із можливостями рушія перетворення calibre. Достатньо зберегти цей файл на вашому комп’ютері і перетворити його на EPUB або AZW3, щоб побачити, на що здатна програма.

calibre автоматично створить таблицю змісту на основі заголовків, якщо ви позначити заголовки стилями Заголовок 1, Заголовок 2 тощо у Microsoft Word. Для перегляду створеної таблиці змісту відкрийте отриману у результаті перетворення електронну книгу у засобі перегляду книг calibre і натисніть кнопку Зміст.

Застарілі файли .doc

Файли у старому форматі .doc можна зберегти як HTML за допомогою Microsoft Word, а потім перетворити отриманий файл HTML за допомогою calibre. Під час збереження даних у форматі HTML вам слід вибрати варіант «Зберегти як вебсторінку, з фільтруванням» («Save as Web Page, Filtered»), оскільки так можна отримати простий код HTML, який буде просто перетворити. Зауважте, що Word створює доволі невдалий код HTML, перетворення якого може бути доволі довгим. Тому вам доведеться потерпіти. Якщо ж у вашій системі встановлено новішу версію Word, ви можете зберегти дані і у форматі docx.

Іншим варіантом дій є використання вільного LibreOffice. Відкрийте ваш файл .doc у LibreOffice і збережіть його у форматі .docx, який можна безпосередньо перетворювати у calibre.

Перетворення документів TXT

У документах TXT немає стандартного способу для позначення форматування тексту, зокрема напівжирного написання, курсиву тощо, або структури документа, зокрема абзаців, заголовків та розділів, але поширеними є деякі правила щодо цих питань. Типово, calibre намагається виконати автоматичне виявлення належного форматування та розмітки на основі цих загальновживаних правил.

Для вхідних даних у форматі TXT передбачено підтримку декількох варіантів обробки, які відрізняються способом виявлення розбиття тексту на абзаци.

Стиль абзацу: Авто

Аналізує текстовий файл і намагається автоматично виявити спосіб задання абзаців. Зазвичай, цей варіант працює добре. Якщо ви отримуєте незадовільні результати, спробуйте один із варіантів із визначенням вручну.

Стиль абзацу: Блок

Припускати, що один або декілька порожніх рядків є межами абзацу:

This is the first.

This is the
second paragraph.

Стиль абзацу: Одинарний

Припускати, що кожен окремий рядок є абзацом:

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

Стиль абзацу: Друк

Припускає, що усі абзаци починаються з відступів (одної табуляції або 2 пробілів). Абзаци закінчуються там, де наступний рядок починається із відступу:

This is the
first.
This is the second.

This is the
third.

Стиль абзацу: Неформатований

Припускає, що у документі немає форматування, але використано жорстке розбиття на рядки. Для відтворення абзаців буде використано пунктуацію і середню довжину рядків.

Стиль форматування: Авто

Спробувати визначити тип форматування автоматично. Якщо у даних немає розмітки, буде застосовано евристичне форматування.

Стиль форматування: Евристичний

Шукає у документі типові заголовки глав, розбиття на сцени та слова, які виокремлено курсивом, і застосовує відповідну розмітку HTML під час перетворення.

Стиль форматування: Розмітка

Крім того, у calibre передбачено підтримку використання для вхідних даних у форматі TXT засобу попередньої обробки відомого як Markdown. Markdown забезпечує додавання базового форматування до документів TXT, зокрема позначення жирним і курсивом тексту, заголовки розділів, таблиці, списки, зміст тощо. Позначення заголовків глав за допомогою початкового # і встановлення виразу XPath для виявлення глав «//h:h1» є найпростішим способом створення належного змісту на основі початкових даних документа TXT. Більше про синтаксис markdown можна дізнатися з сайта daringfireball [https://daringfireball.net/projects/markdown/syntax].

Стиль форматування: Немає

Не застосовує спеціального форматування до тексту. Документ буде перетворено на HTML без будь-яких інших змін.

Перетворення документів PDF

Документи PDF є одним з найгірших початкових форматів для перетворення. У таких документах передбачено фіксований розмір сторінки та фіксоване розташування тексту. Це означає, що програмі дуже важко встановити місце, де завершується один абзац і починається інший. Calibre намагатиметься з’єднати розірвані рядки у абзацах за допомогою змінюваного параметра Коефіцієнт збирання рядків. Це параметр масштабування, який визначає довжину частин рядка, які слід з’єднувати з попередніми. Коректними значеннями цього параметра є десяткові дроби між 0 і 1. Типовим є значення 0.45, трохи менше за половину довжини рядка. Зменште це значення, щоб розширити сферу дії інструмента з’єднування рядків. Збільште значення, щоб звузити сферу його використання. Ви можете скоригувати це значення у вікні параметрів перетворення, вкладка Вхідні дані PDF.

Крім того, у документах PDF часто є верхні і нижні колонтитули, які є частиною документа, яку програма може автоматично включити до тексту документа. Скористайтеся панеллю Пошук з заміною, щоб вилучити такі верхні і нижні колонтитули і усунути пов’язані із ними проблеми. Якщо не усунути верхні і нижні колонтитули, вони можуть завадити нормальній роботі засобу з’єднування рядків абзаців. Щоб дізнатися про те, як скористатися інструментами вилучення верхніх і нижніх колонтитулів, прочитайте розділ Все про використання формальних виразів в calibre.

Деякі обмеження щодо вхідних даних PDF:

- Не передбачено підтримки складних документів, документів із декількома стовпцями тексту та документів, що складаються із зображень.

- Також не передбачено підтримки видобування векторних зображень і таблиць з документа.

- У деяких PDF використовуються спеціальні гліфи для відтворення комбінацій літер ll, ff, fi тощо. Такі комбінації символів не завжди вдається перетворити, — усе залежить від внутрішнього представлення цих символів у PDF.

- Не передбачено підтримки посилань та таблиці змісту.

- Якщо у PDF використано вбудовані шрифти без Unicode для представлення символів поза латиницею, відповідні символи буде показано неправильно

- Деякі файли PDF складаються з фотографій сторінок із оптично розпізнаним текстовим шаром під фотографіями. calibre використає лише текстовий шар, який може значно відрізнятися від того, що ви бачите під час перегляду файла PDF.

- Файли PDF, які використовуються для показу складного тексту, зокрема мов із записом справа ліворуч і математичними формулами, не буде перетворено належним чином.

Ще раз: PDF — дуже, дуже невдалий формат для вхідних даних. Якщо немає альтернативи використанню PDF, варто приготуватися до того, що отримані у результаті перетворення дані матимуть якість від пристойної до непридатної до використання, залежно від початкового PDF.

Збірки коміксів

Збірки коміксів записуються у форматі файла .cbc. Файл .cbc є файлом ZIP, який містить інші файли CBZ/CBR. Крім того, у файлі .cbc має міститися простий текстовий файл і назвою comics.txt і кодуванням вмісту UTF-8. Файл comics.txt має містити список файлів коміксів у файлі .cbc у форматі назва_файла:назва_коміксу, як це показано нижче:

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

Файл .cbc міститиме:

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

calibre автоматично перетворить цей файл .cbc на електронну книгу з таблицею змісту, що вказуватиму не усі записи у comics.txt.

Демонстрація розширеного форматування EPUB

Різноманітні додаткові засоби форматування у файлах EPUB продемонстровано у цьому файлі [https://calibre-ebook.com/downloads/demos/demo.epub]. Файл було створено на основі коду HTML, який було написано вручну, за допомогою calibre і призначено для використання як шаблона для ваших власних спроб створити EPUB.

Початковий код HTML, з якого його було створено, можна знайти у demo.zip [https://calibre-ebook.com/downloads/demos/demo.zip]. Параметри, використані для створення EPUB з ZIP є такими:

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

Зауважте, що оскільки цей файл використовує увесь потенціал EPUB, більшість додаткового форматування не працюватиме у програмах для читання електронних книг, які є менш досконалими за вбудовану до calibre програму для перегляду EPUB.

Перетворення документів ODT
calibre може безпосередньо перетворювати файли ODT (текстові документи OpenDocument). Вам слід використовувати стилі для форматування вашого документа і мінімізувати використання безпосереднього форматування. Вставляючи зображення до вашого документа, ви маєте прив’язувати їх до абзаців. Зображення, які буде прив’язано до сторінок, опиняться на перших сторінках книги під час перетворення.

Щоб уможливити автоматичне визначення глав книги, вам слід позначати їх вбудованими стилями, які мають назви Заголовок 1, Заголовок 2, …, Заголовок 6 (Заголовок 1 відповідає теґу HTML <h1>, Заголовок 2 — теґу <h2> тощо). Під час перетворення у calibre ви можете вказати, який саме стиль ви використали для заголовків за допомогою поля Виявляти глави за. Приклад:

- Якщо ви позначите глави стилем Заголовок 2, вам слід вказати у полі «Виявляти глави за» рядок //h:h2

- Для багаторівневого змісту із розділами, які позначено стилем Заголовок 2, і главами, які позначено стилем Заголовок 3, вам слід вказати //h:h2|//h:h3. На сторінці перетворення змісту вкажіть у полі Рівень 1 змісту //h:h2, а у полі Рівень 2 змісту — //h:h3.

Відомі властивості документа (Заголовок, ключові слова, опис, автор) буде розпізнано. Крім того, calibre використає перше зображення з книги (те, яке не є маленьким і має належне співвідношення розмірів) як зображення обкладинки.

Також передбачено режим розширеного перетворення властивостей, який можна задіяти за допомогою встановлення нетипової властивості opf.metadata (типу «Так чи ні») у значення «Так» у вашому документі ODT (Файл  →  Властивості  →  Нетипові властивості). Якщо calibre буде виявлено цю властивість, програма розпізнаватиме такі нетипові властивості (opf.authors перевизначає властивість автора документа):

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

Окрім цього, ви можете вказати зображення, яке слід використати для обкладинки, назвавши його opf.cover (клацання правою кнопкою на зображенні, :Зображення  →  Параметри  →  Назва) у ODT. Якщо зображення з такою назвою не буде знайдено, буде використано «кмітливий» метод визначення зображення. Оскільки виявлення зображення обкладинки для деяких форматів виведення даних може призвести до подвійних обкладинок, процедурою обробки буде вилучено абзац (лише якщо єдиним його вмістом є зображення) з документа. Але усе це працює лише із іменованим зображенням!

Щоб вимкнути виявлення зображень обкладинки, встановіть для нетипової властивості opf.nocover (типу «Так чи ні») значення «Так» у розширеному режимі.

Перетворення на PDF

Першим, і найважливішим, параметром, який слід визначити при перетворенні даних у формат PDF, є розмір сторінки. Типово, calibre використовує розмір сторінки «U.S. Letter». Ви можете змінити його на інший стандартний розмір або вказати нетиповий розмір у розділі Вивід у PDF діалогового вікна перетворення. Якщо ви створюєте PDF для використання на певному пристрої, ви можете позначити пункт використання розмір у сторінки у профілі виведення. Отже, якщо встановлено профіль виведення на Kindle, calibre створюватиме PDF із розміром сторінки, який є зручним для перегляду на малому екрані Kindle.

Верхні і нижні колонтитули

Ви можете вставити довільний верхній і нижній колонтитул на кожну сторінку PDF, вказавши шаблони верхнього і нижнього колонтитулів. Шаблони — це прості фрагменти коду HTML, які буде оброблено і вставлено туди, де мають бути верхній і нижній колонтитули. Наприклад, щоб на кожній сторінці було показано номери сторінок у нижній частині сторінки посередині, зеленим кольором, скористайтеся таким шаблоном нижнього колонтитула:

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

calibre автоматично замінить _PAGENUM_ на номер поточної сторінки. Ви навіть можете розташувати різні дані на парних і непарних сторінках. Наприклад, наведений нижче шаблон верхнього колонтитула показуватиме назву книги на непарних сторінках і назву книги на парних сторінках:

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

calibre автоматично замінить _TITLE_ і _AUTHOR_ вмістом полів назви і автора документа, який ви перетворюєте. Встановлення для параметра code:justify-content значення flex-end призведе до вирівнювання тексту праворуч.

Ви також можете наказати програмі розмістити текст ліворуч і праворуч та змінити розмір шрифту, як це продемонстровано цим шаблоном верхнього колонтитула:

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

У результаті назву книги буде показано ліворуч, а ім’я автора — праворуч, шрифтом, який буде меншим за шрифт основного тексту книги.

Ви також можете скористатися поточним розділом у шаблонах, як це показано нижче:

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

_SECTION_ буде замінено назвою поточного розділу. Ці назви буде взято із метаданих списку змісту документа (ескізу PDF). Якщо у документі немає списку змісту, цей рядок буде замінено порожнім. Якщо на одній сторінці PDF розміщено декілька розділів, буде використано назву першого з розділів на сторінці. Так само, передбачено змінну із назвою _TOP_LEVEL_SECTION_, якою можна скористатися для отримання назви поточного розділу найвищого рівня.

Ви навіть можете скористатися кодом JavaScript у шаблонах верхнього і нижнього колонтитулів. Наприклад, використання наведеного нижче шаблона призведе до того, що нумерація сторінок починатиметься з 4, а не з 1:

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

Крім того, існують додаткові змінні, якими ви можете скористатися у верхніх і нижніх колонтитулах. Документацію наведено нижче:

- _TOTAL_PAGES_ — загальна кількість сторінок у файлі PDF, корисна для реалізації, наприклад, лічильника поступу.

- _TOP_LEVEL_SECTION_PAGES_ — загальна кількість сторінок у поточному розділі найвищого рівня.

- _TOP_LEVEL_SECTION_PAGENUM_ — номер поточної сторінки у поточному розділі найвищого рівня.

Примітка

Якщо ви додаватимете верхні і нижні колонтитули, не забудьте збільшити верхнє і нижнє поля сторінок за допомогою розділу Виведення до PDF діалогового вікна перетворення.

Придатний до друку список змісту

Ви також можете додати придатний до друку список змісту наприкінці файла PDF, де буде наведено дані щодо номера сторінки для кожного з розділів. Це буде доволі корисно, якщо ви захочете надрукувати PDF на папері. Якщо ви маєте намір читати PDF на електронному пристрої, зміст типово буде створено автоматично, потреби у додаткових сторінках немає.

Ви можете скоригувати вигляд створеного списку змісту за допомогою параметра перетворення «Додаткове CSS» у розділі «Вигляд та поведінка» діалогового вікна перетворення. Типову використану CSS наведено нижче, просто скопіюйте її і внесіть потрібні вам зміни.

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

Нетипові поля сторінок для окремих файлів HTML

Якщо ви виконуєте перетворення до формату EPUB або AZW3 із декількома окремими файлами HTML всередині і хочете змінити поля сторінок для окремого файла HTML, ви можете додати такий стильовий блок до файла HTML за допомогою редактора книг calibre:

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

Далі, у розділі виведення до PDF діалогового вікна перетворення позначте пункт Використовувати поля сторінок з перетворюваного документа. Тепер усі сторінки, які буде створено з цього файла HTML, матимуть поля у 10pt (10 пунктів).

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Редагування електронних книг

Редагування електронних книг

До складу пакунка calibre включено вбудований редактор електронних книг, яким можна скористатися для редагування книг у форматах EPUB та AZW3 (Kindle). У редакторі вам буде показано код HTML і CSS, який використовується у файлах книг, разом із інтерактивною панеллю результатів зміни цього коду, вміст якої відповідатиме внесеним вами змінам. Крім того, у редакторі передбачено різноманітні автоматизовані інструменти для виконання типових завдань із чищення та виправлення коду.

Ви можете скористатися цим редактором так: наведіть вказівник миші на пункт книги у списку calibre, клацніть правою кнопкою миші і виберіть у контекстному меню пункт Редагувати книгу.

Зміст

Базові принципи роботи

Навігатор файловою системою

- Перейменування файлів

- Об’єднання файлів

- Зміна порядку текстових файлів

- Позначення зображення обкладинки

- Вилучення файлів

- Експортування файлів

- Додавання нових зображень, шрифтів та інших елементів або створення порожніх файлів

- Заміна файлів

- Ефективне пов’язування таблиць стилів з файлами HTML

Пошук і заміна

- Збережені результати пошуків

- Режим функцій

- Пошук з ігноруванням міток HTML

Автоматизовані інструменти

- Редагування таблиці вмісту

- Перевірка книги

- Додавання зображення обкладинки

- Вбудовуємо використані шрифти

- Обрізаємо вбудовані шрифти

- Удосконалення пунктуації

- Перетворення властивостей CSS

- Вилучення зайвих правил CSS

- Виправлення HTML

- Удосконалення файлів

- Вставляння вбудованої таблиці змісту

- Встановлення семантики

- Фільтрування даних щодо стилів

- Оновлення внутрішніх частин книги

Контрольні точки

Панель інтерактивного перегляду

- Поділ файлів HTML на частини

Панель «Інтерактивний CSS»

Інші інструменти

- Панель «Зміст»

- Перевірка правопису слів у книзі

- Вставлення спеціальних символів

- Панель ревізії коду

- Перевірка зовнішніх посилань

- Отримання зовнішніх ресурсів

- Розподіл файлів за теками на основі типів

- Імпортування файлів у форматах електронних книг, відмінних від EPUB

- Інструмент «Звіти»

Особливі можливості редактора коду

- Підсвічування синтаксичних конструкцій

- Контекстна довідка

- Автоматичне доповнення

- Фрагменти

Базові принципи роботи

Примітка

Із відеооглядом редактора книг calibre можна ознайомитися тут [https://calibre-ebook.com/uk/demo#tutorials].

Коли ви вперше відкриєте книгу у вікні інструмента «Редагувати книгу», ви побачите панель списку файлів, розташовану ліворуч. Там буде показано окремі файли HTML, таблиці стилів, зображення тощо, з яких складається вміст книги. Щоб розпочати редагування файла, двічі клацніть лівою кнопкою миші на його позначці. Зауважте, що якщо вам потрібно внести зміни, складніші за незначне удосконалення, вам варто ознайомитися із Підручником з HTML [http://html.net/tutorials/html/] і Підручником з CSS [http://html.net/tutorials/css/].

Паралельно із внесенням змін до файлів HTML або CSS у редакторі ви зможете спостерігати за результатами на панелі попереднього перегляду, яку розташовано праворуч. Коли внесення змін буде завершено, натисніть кнопку Зберегти або скористайтеся пунктом меню Файл  →  Зберегти, щоб зберегти внесені вами зміни до файла електронної книги.

Однією з корисних можливостей є Контрольні точки. Перш ніж ви почнете значні зміни у коді, ви можете створити контрольну точку. Контрольна точка збереже поточний стан вашої книги. Якщо у майбутньому ви вирішите, що не варто було вносити якісь зміни, ви можете повернутися до стану, який книга мала на час створення контрольної точки. Для створення контрольної точки скористайтеся пунктом меню Зміни  →  Створити контрольну точку. Крім того, контрольні точки створюватимуться автоматично у відповідь на запуск автоматизованих інструментів, зокрема загального пошуку із заміною. Можливості роботи з контрольними точками доповнюють звичайні механізми скасовування та повторення дій під час редагування окремих файлів. Контрольні точки є корисними, якщо зміни вносяться одразу до декількох файлів у книзі.

Це базовий процес редагування книг — відкрити файл, внести зміни, переглянути результати і зберегти їх. У решті цього підручника буде обговорено різноманітні інструменти і можливості, які нададуть вам змогу ефективно виконувати певні завдання.

Навігатор файловою системою

За допомогою панелі Перегляд файлів ви зможете бачити список файлів у книзі під час редагування. Файли буде впорядковано за категоріями, текстові файли (HTML) буде розташовано на початку списку, за ними буде розташовано файли таблиць стилів (CSS), зображення та ніші файли. Щоб розпочати редагування файла, просто двічі клацніть на його позначці лівою кнопкою миші. Передбачено можливість редагування файлів HTML і CSS, а також файлів зображень. Порядок файлів у списку визначатиметься порядком, у якому ви переглядатимете файли їх вміст під час читання книги. Решта файлів упорядковується за абеткою.

Якщо ви наведете вказівник миші на запис у списку, програма покаже панель підказки із даними щодо розміру файла. У нижній частині вікна програми буде показано повний шлях до файла у книзі. Зауважте, що файли у електронних книгах стискаються, отже остаточний файл книги матиме розмір, який не дорівнюватиме сумі розмірів файлів, з яких він складається.

У книгах багато файлів мають спеціальне призначення. Такі спеціальні файли позначаються піктограмами, розташованими поруч із їхніми назвами. Ці піктограми відповідають спеціальному призначенню файлів. Наприклад, на наведеному ліворуч знімку ви можете бачити файли cover_image.jpg та titlepage.xhtml, які позначено піктограмою обкладинки. Це означає, що маємо справу із зображенням обкладинки і титульною сторінкою, відповідно. Так само, файл content.opf позначено піктограмою метаданих, що означає, що у цьому файлі зберігаються метадані книги. Файл же toc.ncx позначено піктограмою із літерою T, що позначає список змісту (Table of Contents).

Доступ до багатьох дій з окремими файлами можна отримати простим наведенням вказівника миші на позначку файла з наступним клацанням правою кнопкою миші.

Перейменування файлів

Ви можете перейменувати окремий файл, навівши вказівник миші на пункт книги, клацнувши правою кнопкою миші і вибравши у контекстному меню пункт Перейменувати. Перейменування файла автоматично оновить усі посилання і вказівки на нього у книзі. Отже, усе, що вам слід зробити, це вказати нову назву. Решту calibre зробить автоматично.

Крім того, ви можете пакетно перейменовувати файли. Цим можна скористатися, якщо ви хочете, щоб усі файли мали прості назви, сформовані за простим взірцем. Наприклад, можна перейменувати файли HTML так, щоб вони мали назви Глава-1.html, Глава-2.html тощо. Позначте файли, які слід пакетно перейменувати за допомогою утримання натиснутою клавіші Shift або Ctrl і клацання на позначках файлів у списку. Далі, клацніть правою кнопкою миші на одному з позначених файлів і виберіть у контекстному меню пункт Пакетне перейменування позначених файлів. Вкажіть префікс і число, з якого слід почати автоматичну нумерацію, потім натисніть кнопку Гаразд, ось і усе. Крім того, за допомогою вікна пакетного перейменування ви можете перейменувати файли за порядком, у якому вони з’являються у книзі, замість порядку, у якому їх було позначено. Це корисно, наприклад, для перейменовування усіх зображень так, щоб їхня нумерація відповідала порядку, якому вони з’являються у книзі.

Нарешті, ви можете пакетно змінити суфікс назви файла для усіх позначених файлів. Позначте декілька файлів, як вище, клацніть правою кнопкою миші на позначеному і виберіть у контекстному меню пункт Змінити суфікси назв позначених файлів.

Об’єднання файлів

Іноді виникає потреба у об’єднанні двох файлів HTML або двох файлів CSS. Іноді корисно зібрати якісь дані у одному файлі. Втім, цим не слід зловживати, оскільки розташовування усіх даних у одному файлі може призвести до проблем із швидкодією, якщо книгу читають на типовому пристрої для читання електронних книг.

Щоб об’єднати декілька файлів у один, позначте їх, утримуючи натиснутою клавішу Ctrl і клацаючи на позначках файлів (переконайтеся, що ви позначаєте файли одного типу, або файли HTML, або файли CSS, або інші файли). Далі, наведіть вказівник миші на позначку одного з позначених файлів і клацніть правою кнопкою миші, виберіть у контекстному меню пункт об’єднання. Ось і усе, calibre об’єднає файли і автоматично виправить усі посилання та прив’язки, пов’язані із об’єднаним файлом. Зауважте, що об’єднання файлів іноді призводить до зміни оформлення тексту, оскільки у різних файлів можуть бути різні таблиці стилів.

Ви також можете позначити текстові файли, а потім перетягнути і скинути позначки цих текстових файлів на позначку іншого текстового файла, щоб об’єднати скинуті текстові файли із текстовим файлом призначення.

Зміна порядку текстових файлів

Ви можете перевизначити порядок, у якому програма відкриватиме текстові (HTML) файли під час читання книги, простим перетягуванням зі скиданням на панелі навігації файлами або клацанням на пункті файла, який слід пересунути із наступним натисканням модифікаторів Ctrl+Shift у поєднанні із клавішами Вгору, Вниз, Home та End. Для спеціалістів: це називається переупорядкуванням корінця книги.

Зауважте, що вам слід скинути пункти між іншими пунктами, а не на них. Це може бути трохи складно і вимагатиме призвичаєння. Скидання пункту на пункт іншого файла призведе до їхнього злиття.

Позначення зображення обкладинки

У електронних книг типово є зображення обкладинки. Це зображення позначається на панелі навігації файловою системою розташованою поруч із записом зображення піктограмою із брунатною книгою. Якщо ви хочете використати якесь інше зображення для обкладинки, ви можете зробити це навівши вказівник миші на позначку нового зображення обкладинки, клацнувши правою кнопкою миші і вибравши пункт Позначити як зображення обкладинки.

Крім того, у файлах EPUB є річ, яку називають титульною сторінкою. Титульна сторінка — це файл HTML, який працює як титульна сторінка або обкладинка книги. Позначити файл титульної сторінки у форматі HTML можна вибором відповідного контекстного меню запису файла під час редагування EPUB. Переконайтеся, що позначений вами таким чином файл містить лише дані титульної сторінки. Якщо у ньому містяться інші дані, зокрема перша глава, ці дані буде втрачено, якщо користувач спробує перетворити файл EPUB на файл у іншому форматі за допомогою calibre. Так відбувається через те, що під час перетворення calibre припускає що у файлі титульної сторінки міститься лише обкладинка книги і не міститься ніяких інших даних.

Вилучення файлів

Вилучити файли можна за допомогою відповідного пункту контекстного меню або позначенням пунктів файлів із наступним натисканням клавіші Delete. Вилучення файла призводить до вилучення усіх посилань на цей файл з файла OPF, позбавляючи вас від зайвої мороки. Втім, посилання у інших місцях книги вилучено не буде. Для вилучення або заміни таких посилань ви можете скористатися інструментом «Перевірити книгу».

Експортування файлів

Ви можете експортувати файл з книги до якогось іншого місця на вашому комп’ютері. Це корисно, якщо ви хочете попрацювати над файлом окремо, за допомогою спеціалізованих інструментів. Для експортування просто клацніть правою кнопкою миші на файлі і виберіть у контекстному меню пункт Експортувати.

Після завершення роботи над експортованим файлом ви можете повторно імпортувати його до книги. Для цього просто клацніть на файлі правою кнопкою миші ще раз і виберіть у контекстному меню пункт Замінити <назва_файла> файлом…. Це надасть вам змогу замінити файл у книзі раніше експортованим файлом.

Крім того, передбачено можливість копіювання файлів між декількома екземплярами редактора. Позначте файли, які ви хочете скопіювати на панелі Навігація файлами, клацніть правою кнопкою миші на позначеному і виберіть пункт Копіювати позначені файли до іншого екземпляра редактора. Далі, у іншому екземплярі редактора клацніть правою кнопкою миші на панелі Навігація файлами і виберіть пункт Вставити файл з іншого екземпляра редактора.

Додавання нових зображень, шрифтів та інших елементів або створення порожніх файлів

Ви можете додати зображення, шрифт, таблицю стилів тощо з вашого комп’ютера до книги, скориставшись пунктом меню Файл  →  Новий файл. Програма надасть вам змогу імпортувати файл, натиснувши кнопку Імпортувати файл ресурсу, або створити порожній файл HTMLl або таблиці стилів, якщо ви просто введете назву нового файла у поле для введення назви.

Ви також можете імпортувати до книги одразу декілька файлів за допомогою пункту меню «Файл → Імпортувати файли до книги».

Заміна файлів

Замінити наявні файли у книзі дуже просто: позначте пункт файла, який слід замінити, клацніть правою кнопкою миші і виберіть у контекстному меню пункт заміни. Програма автоматично оновить усі посилання та вказівки, якщо файл-замінник матиме назву, яка не збігається із назвою початкового файла.

Ефективне пов’язування таблиць стилів з файлами HTML

Для зручності, ви можете позначити декілька файлів HTML на панелі навігації файловою системою, клацнути правою кнопкою і вибрати у контекстному меню пункт Пов’язати таблиці стилів, щоб наказати calibre автоматично вставити теґи <link> для цих таблиць стилів до усіх позначених файлів HTML.

Пошук і заміна

У редакторі книг передбачено дуже потужний інтерфейс пошуку із заміною, за допомогою якого ви можете шукати і замінювати фрагменти тексту у поточному файлі, у всіх файлах книги і навіть у позначеному фрагменті поточного файла. Пошук може виконуватися у звичайному режимі або у режимі з використанням формальних виразів. Щоб дізнатися більше про використання формальних виразів для розширення можливостей пошуку, зверніться до розділу Все про використання формальних виразів в calibre.

Пошук із заміною можна розпочати за допомогою пункту меню Пошук  →  Знайти/Замінити (має бути редаговано файл HTML або CSS).

Введіть текст, який ви хочете знайти, у поле :guilabel::Знайти, а його замінник у поле Замінити. Ви можете натискати відповідні кнопки, щоб знайти наступний відповідник, замінити поточний відповідник або замінити усі відповідники.

За допомогою спадних списків у нижній частині панелі ви можете виконувати пошук у поточному файлі, в усіх текстових файлах, в усіх файлах стилів або просто в усіх файлах. Ви також можете вибрати режим пошуку: звичайний пошук (за рядками) або пошук за формальним виразом.

Обчислити кількість усіх відповідників виразу можна за допомогою пункту меню Шукати  →  Полічити усі. Обчислення буде виконано для файлів чи фрагментів, які ви вибрали у спадному списку.

Ви також можете перейти до певного рядка у вікні поточного відкритого редактора за допомогою пункту Пошук → Перейти до рядка.

Примітка

Пам’ятайте: щоб скористатися усіма можливостями пошуку із заміною, вам слід використовувати формальні вирази. Див. Все про використання формальних виразів в calibre.

Збережені результати пошуків

Ви можете зберегти вирази для пошуку із заміною (зокрема вирази функціонального режиму), якими часто користуєтеся, і потім знову викликати їх. Щоб зберегти поточні критерії пошуку із заміною, просто клацніть правою кнопкою миші на полі Знайти і виберіть у контекстному меню пункт Зберегти поточні критерії пошуку.

Відкрити список збережених записів пошуку можна за допомогою пункту меню Шукати  →  Збережені критерії пошуку. У відповідь на вибір цього пункту програма покаже вам список виразів для пошуку із заміною, якими ви можете скористатися. Ви навіть можете вибрати декілька записів у списку одразу, утримуючи натиснутою клавішу Ctrl із одночасним клацанням на пунктах списку. Таким чином, ви можете виконувати декілька дій із пошуку з заміною одним натисканням кнопки.

Режим функцій

За допомогою режиму функцій формального виразу ви можете визначати довільні потужні функції Python, які виконуватимуться під час кожної дії із пошуку з заміною. У цьому режимі ви можете виконувати майже будь-які дії з текстом. Щоб дізнатися більше про цей режим, зверніться до розділу Функціональний режим для пошуку і заміни у редакторі.

Пошук з ігноруванням міток HTML

Крім того, передбачено особливий інструмент для пошуку тексту із ігноруванням усіх теґів HTML. Наприклад, якщо у книзі є код HTML Зверніть увагу на <i>слово</i>., ви можете шукати текст на слово, і його буде знайдено, навіть попри те, що у коді він містить теґи <i>. Доступ до цього інструмента можна отримати за допомогою пункту меню Шукати  →  Пошук з ігноруванням розмітки HTML.

Автоматизовані інструменти

Передбачено різноманітні засоби редагування, які допоможуть вам виконати типові завдання. Пункти цих засобів розташовано у меню Інструменти.

Редагування таблиці вмісту

Передбачено спеціалізований інструмент для спрощення редагування таблиці змісту. Запустити його можна за допомогою пункту Інструменти → Зміст → Змінити зміст.

Інструмент редагування змісту покаже вам поточний список змісту книги (якщо такий існує) у лівій частині вікна. Щоб змінити текст запису змісту, просто двічі клацніть лівою кнопкою миші на відповідному пункті у списку. Змінити порядок пунктів у списку можна простим перетягуванням зі скиданням або за допомогою кнопок, розташованих праворуч.

Для книг, у яких ще немає списку змісту, інструмент надасть вам змогу вибрати варіант автоматичного створення списку змісту на основі тексту книги. Ви можете створити зміст на основі заголовків у документі, на основі посилань, окремих файлів тощо.

Ви можете редагувати окремі записи: клацніть на пункті запису, потім натисніть кнопку Змінити місце, на яке вказує цей запис. У відповідь буде показано мініпанель перегляду книги. Просто пересуньте вказівник миші на панель перегляду книги і клацніть там, куди має вказувати запис. Товста зелена лінія покаже вам місце. Натисніть кнопку Гаразд, коли місце буде вказано правильно.

Перевірка книги

Інструмент Перевірити книгу виконує пошук проблемних місць у книзі. Такі проблемні місця можуть призвести до неналежного показу вмісту книги читачеві на пристроях для читання електронних книг. Скористатися цим інструментом можна за допомогою пункту меню Інструменти  →  Перевірити книгу.

Усі знайдені проблеми буде показано у чудовому, простому у користуванні списку. Після натискання будь-якого пункту у списку програма покаже вам деякі довідкові дані щодо помилки, а також запропонує вам автоматично виправити проблему, якщо це можливо. Ви також можете двічі клацнути лівою кнопкою миші на пункті помилки, щоб відкрити відповідний код у редакторі з метою його виправлення.

Серед перевірок, які виконує програма, такі:

- Пошук помилок у форматуванні HTML. Буде повідомлено про усі фрагменти коду HTML, які не є коректними з огляду на правила XML. Виправлення таких помилок гарантує належну роботу визначеного вами форматування в усіх контекстах. Крім того, у calibre передбачено автоматичне виправлення таких помилок. Втім, автоматичне виправлення іноді може мати несподівані наслідки, отже ним слід користуватися з осторогою. Як завжди, перед автоматичним виправленням буде створено контрольну точку, отже ви зможете без проблем скасувати усі зміни. Автовиправлення працює, виконуючи обробку розмітки за алгоритмом HTML5, який має високий рівень надійності, і перетворюючи код у коректний XML.

- Пошук помилково форматованих або невідомих стилів CSS. Засіб перевірки повідомить вам про увесь код CSS, який є некоректним або який має властивості, які не визначено у стандарті CSS 2.1 (а також деякі з CSS 3). CSS буде перевірено в усіх таблицях, вбудованих атрибутах стилів та у теґах <style> у файлах HTML.

- Помилкові посилання. Програма повідомляє про посилання всередині книги, які вказують на файли, які не вдалося виявити.

- Файли без посилань. Створюється звіт щодо файлів у книзі, на які не посилається жоден інший файл або яких немає у вмісті книги.

- Різноманітні типові проблеми у файлах OPF, зокрема дублювання записів вмісту або маніфесту, помилкові idref або теґи обкладинок, пропущені обов’язкові розділи тощо.

- Різноманітні перевірки сумісності щодо відомих проблем, які можуть призвести до неможливості читання книги на пристроях для читання електронних книг.

Додавання зображення обкладинки

Додати зображення обкладинки до книги дуже просто: скористайтеся пунктом меню Інструменти  →  Додати обкладинку. Ви зможете або вибрати як обкладинку наявне зображення у книзі або імпортувати нове зображення до книги із зробити його зображенням обкладинки. Під час редагування файлів EPUB буде автоматично створено обгортку-код HTML для зображення обкладинки. Якщо у книги вже було зображення обкладинки, його при визначенні нового зображення буде замінено. Інструмент також автоматично подбає про належне позначення файлів зображень обкладинки у коді OPF.

Вбудовуємо використані шрифти

Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Вбудувати використані шрифти. Інструмент шукає усі шрифти, які використано у книзі і, якщо їх ще не було вбудовано, шукає відповідні файли на вашому комп’ютері і вбудовує знайдені файли до книги. Будь ласка, переконайтеся, що у вас є відповідні права на вбудовування шрифтів із комерційними умовами ліцензування, перш ніж вбудовувати такі шрифти до книг.

Обрізаємо вбудовані шрифти

Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Обрізати вбудовані шрифти. Він надає змогу обмежити набори символів усіх вбудованих до книги шрифтів лише тими гліфами, які потрібні для відтворення тексту книги. Зазвичай, таким чином можна зменшити розмір файлів шрифтів приблизно на 50%. Втім, слід зауважити, що після обрізання шрифтів стане неможливим додавання тексту, символів якого немає у обрізаному шрифті. Шрифт просто не працюватиме з таким текстом. Отже, цю дію слід виконувати наприкінці процедури редагування книги.

Удосконалення пунктуації

Виконати перетворення звичайних дефісів, багатокрапок, тире з декількох дефісів тощо відповідно до типографських стандартів. Зауважте, що використання цього алгоритму може іноді призводити до некоректних результатів, особливо якщо на початку фрагментів тексту використано одинарні лапки. Доступ до цього пункту можна отримати за допомогою пункту меню Інструменти  →  Удосконалити пунктуацію.

Перетворення властивостей CSS

Створити правила для перетворення стилів книги. Наприклад, створити правило для перетворення усього тексту, позначеного червоним кольором, на текст зеленим кольором, або подвоєння розміру шрифту усього тексту у книзі, або зробити текст певною гарнітурою шрифту курсивним.

Створювати правила просто. Правила записуються у природному для мови форматі, ось так:

- Якщо властивість color має значення red змінити його на green

- Якщо властивість font-size має будь-яке значення, помножити це значення на 2

Доступ до цього інструменту можна отримати за допомогою пункту меню Інструменти  →  Перетворити стилі.

Вилучення зайвих правил CSS

Вилучити усі невикористані правила CSS з таблиць стилів та теґів <style>. У деяких книгах, створених на основі шаблонів, багато зайвих правил CSS, які не використовуються для форматування даних. Ці правила можуть уповільнити обробку програмами для читання. Доступ до цього засобу можна отримати за допомогою пункту меню Інструменти  →  Вилучити невикористані CSS.

Виправлення HTML

Цей інструмент просто перетворює код HTML, який не вдається обробити за правилами XML, на добре форматований код XML. Помилкове форматування коду XML є доволі поширеною вадою електронних книг. Цей інструмент просто автоматизує виправлення коду HTML книг. В основі роботи інструмента лежить використання алгоритму HTML5 (цей алгоритм використовується в усіх сучасних програмах для перегляду сторінок інтернету) і наступне перетворення результату у код XML. Зауважте, що автоматичне виправлення може іноді призвести до небажаних результатів. Якщо хочете, можете скористатися інструментом Перевірити книгу, який ми обговорювали вище, щоб виявити і виправити вручну проблеми у коді HTML. Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Виправити HTML.

Удосконалення файлів

Цей інструмент використовується для автоматичного форматування усіх файлів HTML і CSS так, щоб вони «мали добрий вигляд». У коді автоматично розставляються відступи, щоб рядки виглядали красиво, вставляються порожні рядки там, де варто їх додати, тощо. Зауважте, що під час поліпшення вигляду коду також автоматично виправляються помилки у HTML/CSS. Тому, якщо ви не хочете, щоб було виконано автоматичне виправлення помилок, спершу скористайтеся інструментом Перевірити книгу, щоб виправити усі проблеми, а вже потім удосконалюйте вигляд коду. Доступ до інструмента можна отримати за допомогою пункту меню Інструменти  →  Удосконалити усі файли.

Примітка

У HTML у будь-якому тексті можуть бути значні пробільні інтервали, які визначаються інструкцією white-space у CSS. Через це, удосконалення коду потенційно може змінити вигляд обробленого HTML. Щоб уникнути небажаних змін, алгоритм удосконалення обробляє лише ті теґи рівня блоку, які містять інші теґи рівня блоку. Отже, наприклад, пробільні інтервали у тексті у тезі <p> не буде змінено. Але теґ <body>, який містить інші теґи <p> і <div>, буде удосконалено. Це іноді може означати, що певний файл взагалі не буде удосконалено, оскільки у ньому не міститься відповідних теґів рівня блоку. У таких випадках ви можете спробувати інші інструменти удосконалення, які є менш розбірливими, наприклад HTML Tidy [https://infohound.net/tidy/].

Вставляння вбудованої таблиці змісту

Зазвичай, у електронних книгах список змісту відділено від основного тексту, доступ до нього типово здійснюється за допомогою спеціальної кнопки або пункту меню Зміст на пристрої для читання електронних книг. Крім того, ви можете наказати calibre автоматично створити вбудований список змісту, який стане частиною тексту книги. Такий список змісту буде створено на основі поточного визначеного списку змісту.

Якщо ви спробуєте скористатися цим інструментом декілька разів, кожен його виклик призводитиме до того, що раніше створений список змісту замінюватиметься новим. Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Зміст  →  Вставити вбудований зміст.

Встановлення семантики

Цей інструмент використовується для встановлення семантики у файлах EPUB. Елементами семантики є посилання у файлі OPF, які визначаються певні місця у книзі як такі, що мають спеціальне значення. Ви можете скористатися ними для визначення передмови, присвяти, обкладинки, змісту тощо. Просто виберіть тип семантичної інформації, який ви хочете визначити, а потім виберіть місце у книзі, на яке вказуватиме посилання. Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Встановити семантику.

Фільтрування даних щодо стилів

Цим інструментом можна скористатися для спрощення вилучення вказаних властивостей стилю CSS з усієї книги. Ви можете повідомити інструменту, які саме властивості слід вилучити, наприклад, color, background-color, line-height, і він вилучити їх усюди — із таблиць стилів, теґів <style> і вбудованих атрибутів style. Після вилучення даних щодо стилю буде показано резюме щодо усіх внесених змін; отже ви зможете побачити, що саме було змінено. Доступ до цього інструмента можна отримати за допомогою пункту меню Інструменти  →  Фільтрувати інформацію щодо стилів.

Оновлення внутрішніх частин книги

Цим інструментом можна скористатися для оновлення, якщо це можливо, внутрішніх частин книги. Наприклад, вні може оновити книги EPUB 2 до версії формату EPUB 3. Доступ до інструмента можна отримати за допомогою пункту Оновити внутрішні частини.

Контрольні точки

Контрольні точки — спосіб позначити поточний стан книги як «особливий». Далі, ви можете внести до книги будь-які зміни і, якщо вони вам не сподобаються, повернутися до стану для якого було створено контрольну точку. Контрольні точки створюються автоматично кожного разу, коли ви використовуєте автоматизовані інструменти, описані у попередньому розділі.

Створити контрольну точку можна за допомогою пункту меню Зміни  →  Створити контрольну точку. Повернутися до попередньої контрольної точки можна за допомогою пункту меню Зміни  →  Відновити …

Функціональні можливості із роботи з контрольними точками доповнюють звичайні можливості зі скасовування і повторення дій під час редагування окремих файлів. Зокрема контрольні точки корисні, якщо зміни торкаються декількох файлів у книзі або якщо ви хочете одразу скасувати велику групу пов’язаних змін.

Зі списком доступних контрольних точок можна ознайомитися за допомогою пункту меню Перегляд  →  Контрольні точки. Ви можете порівняти поточний стан книги із визначеною вами контрольною точкою за допомогою програми Порівняння електронних книг: достатньо позначити якусь контрольну точку і натиснути кнопку Порівняти. За допомогою кнопки Відновити можна відновити стан книги на момент створення вибраної контрольної точки, скасувавши усі зміни, які було внесено до книги з моменту створення контрольної точки.

Панель інтерактивного перегляду

За допомогою панелі Попередній перегляд вмісту ви зможете переглядати вміст різноманітних файлів. Панель попереднього інтерактивного перегляду надає вам змогу одразу бачити зміни (із затримкою у одну чи дві секунди). Під час редагування файлів HTML або CSS вміст панелі автоматично оновлюється і відповідає поточному стану коду. Пересування курсора на панелі редактора призводить до зміни фокусу на панелі попереднього перегляду: програма показує вам саме те місце у книзі, де у коді перебуває курсор. Клацанням на панелі попереднього перегляду ви можете пересунути курсор у відповідне місце коду. Якщо ви клацнете на посиланні на інший файл у книзі, цей файл буде автоматично відкрито на панелі редагування і панелі попереднього перегляду.

Ви можете вимкнути автоматичну синхронізацію позицій та інтерактивний перегляд змін. Відповідні кнопки розміщено у нижній частині панелі попереднього перегляду. Інтерактивне оновлення вмісту на панелі попереднього перегляду відбувається, лише коли ви не набираєте щось у вікні редактора. Так зроблено, щоб не відволікати вас і не уповільнювати роботу очікуванням обробки вигляду тексту.

На панелі попереднього перегляду буде показано текст таким, яким його можна буде побачити під час читання книги. Втім, панель попереднього перегляду не є замінником справжнього тестування вашої книги на пристрої для читання електронних книг. У неї одночасно і більше і менше можливостей за справжню програму для читання книг. Вона поблажливіша до помилок і неточної розмітки за більшу частину пристроїв для читання книг. Крім того, на панелі попереднього перегляду не буде показано полів сторінки, розривів сторінок та вбудованих шрифтів, для яких використовуються замінники за назвами шрифтів. Користуйтеся панеллю попереднього перегляду, доки ви працюєте над книгою. Але коли ви завершите роботу, перегляньте результат на справжньому пристрої для читання електронних книг або його програмному емуляторі.

Примітка

На панелі попереднього перегляду не передбачено підтримки таких вбудованих шрифтів, у яких назва шрифту у файлі шрифту не збігається із назвою шрифту у правилі @font-face CSS. Ви можете скористатися інструментом Перевірити книгу для того, щоб швидко знайти і виправити такі проблеми зі шрифтами.

Поділ файлів HTML на частини

Ще одним, ймовірно неочевидним, застосуванням панелі попереднього перегляду є поділ довгих файлів HTML. Під час перегляду файла, який ви хочете поділити, натисніть кнопку Режим поділу, яку розташовано під панеллю попереднього перегляду . Далі просто наведіть вказівник на місце у книзі, де слід виконати поділ файла, і клацніть лівою кнопкою миші. Місце поділу буде показано товстою зеленою лінією, доки ви рухатимете вказівник миші. Поділ буде виконано, щойно ви клацнете лівою кнопкою миші.

Після поділу файла програма автоматично оновити усі посилання і вказівки, які стосувалися нижньої частини файла, і відкриє новий файл у вікні редактора.

Ви можете також поділити окремий файл HTML автоматично у декількох місцях. Достатньо клацнути правою кнопкою миші на коді файла у редакторі і вибрати у контекстному меню пункт Поділити у декількох місцях. Так ви зможете дуже просто поділити великий файл за теґами заголовків або усіма теґами, які належать до певного класу, тощо.

Панель «Інтерактивний CSS»

На панелі Інтерактивний CSS вам буде показано усі правила стилів, які застосовуються до теґу, який ви зараз редагуєте. Буде показано назву теґу разом із номером рядка у редакторі, а також список відповідних правил стилів.

Це чудовий спосіб швидко переглянути правила стилів, які стосуються будь-якого з теґів. На панелі перегляду буде також показано придатні до натискання посилання (позначені синім кольором), натискання яких переноситиме вас до місця, де було визначено стиль, якщо ви захочете внести зміни до правил стилів. Буде показано правила стилів, які застосовуються безпосередньо до теґу, а також правила, які було успадковано від батьківських теґів.

На панелі також буде показано, якими є остаточні обчислені стилі для теґу. У списку властивості, які було перевизначено правилами вищої пріоритетності, буде позначено перекресленням.

Ви можете увімкнути панель інтерактивного CSS за допомогою пункту меню Перегляд  →  Інтерактивний CSS.

Інші інструменти

У програмі передбачено ще декілька інструментів, які можуть виявитися корисними для редагування книг.

Панель «Зміст»

На панелі перегляду списку змісту буде показано поточний список змісту книги. Подвійне клацання на будь-якому з пунктів відкриє місце, на яке вказує пункт, у редакторі коду. Ви можете клацнути правою кнопкою миші, щоб вибрати у меню пункт редагування списку змісту, пункт оновлення перегляду або пункт розгортання або згортання усіх записів. Доступ до цієї панелі перегляду можна отримати за допомогою пункту меню Перегляд  →  Зміст.

Перевірка правопису слів у книзі

Скористатися засобом перевірки правопису можна за допомогою пункту меню Інструменти → Перевірити правопис.

Слова буде показано із даними щодо кількості використання цих слів у книзі та мови, до якої вони належать. Дані щодо мови буде взяти з метаданих книги та атрибутів lang у файлах HTML. Це надає змогу засобу перевірки правопису працювати належним чином, навіть у книгах, які містять текст декількома мовами. Наприклад, у наведеному нижче фрагменті HTML, слово color буде перевірено за допомогою словника американської англійської, а слово colour — за допомогою словника британської англійської:

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

Примітка

Ви можете двічі клацнути на пункті слова, щоб програма позначила наступний випадок використання слова у редакторі. Це корисно, якщо ви хочете змінити слово вручну або переглянути контекст, у якому його вжито.

Щоб замінити слово, просто двічі клацніть на одній із пропозицій його правильного написання, показаних праворуч, або введіть ваш власний варіант написання і натисніть кнопку Замінити позначене слово на. Програма замінить усі випадки використання слова у книзі. Ви також можете клацнути правою кнопкою миші на слові у основному списку слів, щоб зручно замінити слово на варіант з контекстного меню.

Ви можете наказати засобу перевірки правопису ігнорувати слово протягом поточного сеансу перевірки, натиснувши кнопку Ігнорувати. Ви також можете додати слово до словника користувача, натиснувши кнопку Додати до словника. У засобі перевірки правопису передбачено можливість використання декількох словників користувача. Ви можете вибрати словник, до якого буде додано слово.

Крім того, ви можете наказати засобу перевірки правопису показати усі слова у книзі, а не лише ті, де виявлено помилки. Це корисно для визначення найчастіше вживаних слів у книзі та виконання простого пошуку із заміною окремих слів.

Примітка

Якщо ви внесли до книги якісь зміни шляхом редагування файлів, доки було відкрито вікно засобу перевірки правопису, вам слід натиснути кнопку Оновити у вікні засобу перевірки. Якщо ви цього не зробите, і продовжите користуватися застарілими даними, зміни, які було внесено у вікні редактора, може бути втрачено.

Примітка

Щоб виключити окремий файл з перевірки правопису, ви можете скористатися кнопкою Виключити файли або додати такий коментар одразу після початкового теґу у файлі:

<!-- calibre-no-spell-check -->

Додавання нових словників

Засіб перевірки правопису постачається із вбудованими словникам для англійської та іспанської мов. Ви можете встановити власні словник за допомогою вкладки Налаштування  →  Редактор  →  Керування словниками перевірки правопису. Засіб перевірки правопису може використовуватися словники програми LibreOffice (у форматі файлів .oxt). Ви можете отримати ці словники зі сховища розширень LibreOffice [https://extensions.libreoffice.org/?Tags%5B%5D=50].

Вставлення спеціальних символів

Ви можете вставляти символи, які важко вводити з клавіатури, за допомогою пункту меню Зміни  →  Вставити спеціальний символ. У відповідь буде показано таблицю символів Unicode. Просто натисніть пункт символу, який ви хочете ввести. Якщо ви утримуватимете натиснутою клавішу Ctrl, доки клацатимете, вікно буде автоматично закрито після вставляння вибраного символу. Цим інструментом можна скористатися для введення спеціальних символів до основного тексту або до будь-якого іншого елемента інтерфейсу користувача, зокрема до поля засобу пошуку із заміною.

Оскільки спеціальних символів дуже багато, ви можете визначити ваші Улюблені символи, які буде показано першими. Просто наведіть вказівник миші на відповідний символ, клацніть правою кнопкою миші і виберіть з контекстного меню пункт позначення символу як улюбленого. Нарешті, ви можете перевпорядкувати символи у списку улюблених: натисніть кнопку Перевпорядкувати улюблені, а потім упорядкуйте символи перетягуванням зі скиданням.

Ви також можете безпосереднього вводити спеціальні символи з клавіатури. Для цього ви можете ввести код символу у Unicode (у шістнадцятковому форматі), а потім натиснути комбінацію клавіш Alt+X, яка перетворить раніше введений код на відповідний символ. Наприклад, щоб ввести ÿ, вам слід набрати ff, а потім натиснути комбінацію клавіш Alt+X. Щоб ввести нерозривний пробіл, вам слід ввести a0 і натиснути Alt+X, щоб ввести горизонтальну багатокрапку, вам слід ввести 2026 і натиснути Alt+X, тощо.

Нарешті, ви можете вводити спеціальні символи за допомогою іменованих замінників HTML. Наприклад, після введення &nbsp; цю комбінацію символів буде замінено на нерозривний пробіл одразу після введення крапки з комою. Заміну буде здійснено лише після введення крапки з комою.

Панель ревізії коду

На цій панелі буде показано код HTML та таблицю стилів CSS, які стосуються поточного елемента. Відкрити панель можна за допомогою клацання правою кнопкою миші на потрібному місці на панелі попереднього перегляду із наступним вибором у контекстному меню пункту Перевірити. Ви зможете переглянути код HTML елемента і, що важливіше, стилі CSS, які до нього застосовано. Ви навіть можете редагувати стилі і негайно бачити результат застосування внесених змін. Зауважте, що редагування стилів не змінює вмісту книги, а лише надає вам змогу трохи поекспериментувати. Можливість інтерактивного редагування у Інспекторі усе ще перебуває на стадії розробки.

Перевірка зовнішніх посилань

Ви можете скористатися цим інструментом для того, щоб перевірити усі посилання, які посилаються на сайти інтернету, у книзі. Програма спробує відвідати усі посилання на зовнішні сайти і, якщо отримати дані не вдасться, у зручному форматі повідомить вам про помилкові посилання, які вам варто виправити.

Отримання зовнішніх ресурсів

Цим інструментом ви можете скористатися для автоматичного отримання будь-яких зображень, таблиць стилів та інших даних, які не включено до книги (тобто ті, на які книга посилається як на дані у інтернеті). Програма знайде усі такі ресурси і автоматично отримає дані, додасть їх до книги і замінить усі посилання на них на посилання на отримані файли.

Розподіл файлів за теками на основі типів

Часто під час редагування файлів EPUB, отриманих деінде, ви можете виявити, що файли в EPUB упорядковано якось дивно, за різними підтеками. За допомогою цього інструмента ви можете автоматично пересунути усі файли до відповідних їм підтек на основі даних про типи цих файлів. Доступ до інструмента можна отримати за допомогою пункту меню Інструменти  →  Розподілити за теками. Зауважте, що цей інструмент змінює лише спосіб упорядковування файлів в EPUB, він не змінює спосіб, у який ці файли буде показано на панелі навігації файлами.

Імпортування файлів у форматах електронних книг, відмінних від EPUB

До редактора включено можливість імпортування файлів у деяких інших форматах безпосередньо у новий файл EPUB, без проміжного повноцінного перетворення. Це, зокрема, корисно для безпосереднього створення файлів EPUB з ваших власноруч створених файлів HTML. Скористатися цією можливістю можна за допомогою пункту меню Файл  →  Імпортувати файл HTML або DOCX як нову книгу.

Інструмент «Звіти»

До редактора включено чудовий інструмент Звіти (доступний за допомогою пункту меню Інструменти  →  Звіти), за допомогою якого можна переглядати резюме щодо файлів, зображень, посилань, слів, символів та стилів, використаних у книзі. З кожним рядком звіту пов’язано посилання. Подвійне клацання лівою кнопкою миші на рядку у списку призведе до переходу до місця у книзі, де відповідний елементи книги використано або визначено. Наприклад, на панелі Посилання ви можете двічі клацнути на записі у стовпчику Джерело, щоб перейти до місця, де посилання було визначено, і двічі клацнути на записі у стовпчику Призначення, щоб перейти до місця, на яке вказує посилання.

Особливі можливості редактора коду

Редактор HTML у calibre є дуже потужним. У ньому передбачено багато можливостей, які спрощують редагування коду HTML (і CSS).

Підсвічування синтаксичних конструкцій

У вікні редактора HTML передбачено дуже зручну систему підсвічування синтаксичних конструкцій. Серед її можливостей такі:

- Текст між теґами напівжирного, курсивного стилю та стилю заголовків буде позначено напівжирним або курсивним стилем.

- Під час пересування курсора кодом HTML програма підсвічуватиме теґи HTML. Ви можете переходити до початкового або кінцевого теґу за допомогою клавіатурних скорочень Ctrl+{ і Ctrl+}. Так само, ви можете позначити вміст теґу за допомогою комбінації клавіш Ctrl+Alt+T або Ctrl+Shift+T.

- Некоректні конструкції мови HTML буде підкреслено червоною лінією

- Помилки у правописі тексту у теґах HTML і атрибутах, зокрема атрибутах title, буде позначено. Перевірка правопису враховуватиме мову, яку визначено значенням атрибута lang поточного теґу та загальними параметрами мови книги.

- Буде підсвічено код CSS, вбудований за допомогою теґів <style>.

- Спеціальні символи, які важко виокремити простим поглядом, зокрема нерозривні пробіли, різні типи тире тощо, буде підсвічено.

- У посиланнях на інші файли у теґах <a> tags, <img> і <link> назви файлів буде позначено іншим кольором. Якщо файла, на який посилаються, не існує, назву файла буде підкреслено червоним кольором.

Контекстна довідка

Щоб переглянути довідкові дані щодо теґу або властивості, клацніть правою кнопкою миші на назві теґу HTML або назві властивості CSS.

Щоб автоматично відкрити файл у редакторі, натисніть і утримуйте клавішу Ctrl і клацніть на назві файла всередині теґу посилання. Так само, утримання натиснутою клавіші Ctrl під час клацання на назві класу надасть вам змогу перейти до першого стилістичного правила, яке відповідає теґу і класу.

Клацанням правою кнопкою миші на назві класу у файлі HTML ви можете отримати доступу до перейменування класу, зміни усіх екземплярів класу у книзі і усіх її таблицях стилів.

Автоматичне доповнення

Під час редагування електронної книги одним із найбільш марудних завдань є створення посилань на інші файли у книзі, таблиці стилів CSS або зображення. Вам потрібно визначити належну назву файла і відносний шлях у файлі. Засіб автоматичного доповнення у редакторі зробить ваше завдання набагато простішим.

Під час введення назви файла редактор автоматично пропонуватиме вам варіанти його продовження. Просто натисніть клавішу Tab, щоб вибрати належну назву файла. Редактор навіть пропонуватиме вам варіанти посилань, на прив’язки у іншому файлі HTML. Після введення вами символу # редактор покаже список усіх прив’язок у файлі призначення із невеличкими фрагментами тексту, щоб вам простіше було вибрати належну прив’язку.

Зауважте, що на відміну від більшості інших систем автоматичного доповнення, система автоматичного доповнення редактора використовує встановлення відповідності за підпослідовностями символів. Це означає, що ви можете ввести дві або три літери з будь-якої частини назви файла, щоб мати змогу вибрати належний варіант повної назви файла. Нехай, наприклад, вам потрібно ввести назву файла ../images/arrow1.png. Ви можете просто ввести ia1 і натиснути Tab, щоб завершити введення цієї назви файла. Під час пошуку відповідників система автоматичного доповнення надає пріоритет літерам на початку слова або одразу після роздільника у запису шляху до файла. Щойно ви звикнете до цієї системи, вона надасть вам змогу заощаджувати багато часу і зусиль.

Фрагменти

У редакторі книг calibre передбачено підтримку фрагментів. Фрагмент — це текст, який або часто використовується, або містить багато надлишкового тексту. Редактор надає вам змогу вставляти фрагмент, натиснувши лише декілька клавіш. Фрагменти є доволі потужним інструментом, із багатьма можливостями, зокрема замінниками, між якими можна переходити, автоматичним віддзеркаленням повторюваних слів тощо. Докладніший опис можна знайти у розділі Фрагменти.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Редагування електронних книг »

- Функціональний режим для пошуку і заміни у редакторі

Функціональний режим для пошуку і заміни у редакторі

У інструменті пошуку із заміною редактора передбачено підтримку функціонального режиму. У цьому режимі ви можете поєднувати формальні вирази (див. Все про використання формальних виразів в calibre) із довільними потужними функціями Python для виконання усіх типів розширеної обробки тексту.

У стандартному режимі формальних виразів для пошуку з заміною ви вказуєте формальний вираз для пошуку і шаблон, який буде використано для заміни усіх відповідників. У функціональному режимі, замість використання фіксованого шаблона, ви вказуєте довільну функцію, яку написано мовою програмування Python [https://docs.python.org]. Це надає вам змогу виконувати багато завдань, які неможливо виконати за допомогою простих шаблонів.

Методики використання функціонального режиму та синтаксис буде описано у форматі прикладів, які продемонструють вам, як створити функції, які виконуватимуть усе більш складні завдання.

Автоматичне виправлення регістру літер у заголовках документа

Тут ми підсилимо одну із вбудованих функцій редактора, реалізувавши автоматичну заміну регістру символів усього тексту у теґах заголовків на регістр заголовків:

Find expression: <([Hh][1-6])[^>]*>.+?</\1>

Для цієї функції просто виберіть вбудовану функцію Title-case text (ignore tags). Ця функція змінює заголовки ось так: з <h1>якийсь ЗАГОЛОВОК</h1> виходить <h1>Якийсь Заголовок</h1>. Вона працюватиме, навіть якщо у теґах заголовка місяться інші теґи HTML.

Ваша перша нетипова функція — кмітливе розставляння дефісів

Справжня потужність функціонального режиму полягає у можливості створення власних функцій для довільної обробки тексту. Інструмент кмітливої пунктуації у редакторі не чіпає окремих дефісів, отже ви можете скористатися цією функцією для заміни їх на тире.

Щоб створити нову функцію, просто натисніть кнопку Створити/Редагувати для створення нової функції і скопіюйте код Python, наведений нижче.

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return match.group().replace('--', '—').replace('-', '—')

Кожна нетипова функція пошуку з заміною повинна мати неповторну назву і складатися з функції python із назвою replace, яка приймає усі аргументи, вказані вище. Для початку, пропустимо усі різні аргументи функції replace(). Зосередимо нашу увагу на аргументі match. Він представляє відповідник рядка пошуку під час пошуку з заміною. Повну документацію щодо нього можна знайти тут [https://docs.python.org/library/re.html#match-objects]. match.group() просто повертає увесь відповідний текст, і усе, що нам лишається зробити, це замінити дефіси у цьому тексті на тире. Спочатку замінюємо подвійні дефіси, а потім одинарні.

Скористайтеся цією функцією у формальному виразі пошуку:

>[^<>]+<

І вона замінити усі дефіси на тире, але лише у справжньому тексті, а не у визначеннях теґів HTML.

Потужність режиму функцій — використання словника для перевірки правопису для виправлення слів із помилковими дефісами

Часто у електронних книгах, які створено на основі даних оптичного розпізнавання тексту друкованих книг, містяться слова із помилковими переносами — слова, які було розділено дефісом наприкінці рядка друкованої сторінки. Давайте напишемо просту функцію, яка автоматично виявлятиме і виправлятиме такі слова.

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

Скористайтеся цією функцією із тим самим виразом для пошуку, що і раніше, а саме:

>[^<>]+<

І вона чарівним чином виправить усі слова із помилковими дефісами у тексті книги. Основним трюком тут є використання одного з корисних аргументів функції replace, а саме аргументу dictionaries. Цей аргумент посилається на словники, які редактор використовує для перевірки правопису тексту книги. Ця функція шукає слова, які відокремлено дефісом, вилучає дефіс і перевіряє, чи розпізнається словником з’єднане слово. Якщо слово розпізнається, початкові слова буде замінено об’єднаним словом без дефіса.

Зауважте, що одним із обмежень цієї методики є те, що вона працюватиме лише для одномовних книг, оскільки, типово, dictionaries.recognized() використовує основну мову книги.

Автоматична нумерація розділів

Тепер ми розглянемо дещо інший приклад. Припустімо, що у вашому файлі HTML багато розділів, кожен з яких має заголовок у тезі <h2>, який виглядає так: <h2>Якийсь текст</h2>. Ви можете створити нетипову функцію, яка автоматично нумеруватиме такі заголовки послідовними номерами розділів, щоб виходило ось таке: <h2>1. Якийсь текст</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'

Скористайтеся цим у виразі для пошуку:

(?s)(<h2[^<>]*>)(.+?</h2>)

Розташуйте курсор на початку файла і натисніть кнопку Замінити усі.

У цій функції використано ще один з корисних додаткових аргументів replace(): аргумент number. Під час виконання дії Замінити усе число автоматично збільшуватиметься для кожного наступного відповідника.

Ще однією новою можливістю є використання replace.file_order — встановлення значення 'spine' означає, що якщо пошук виконуватиметься для декількох файлів HTML, вони оброблятимуться у порядку, у якому з’являтимуться у тексті книги. Див. Вибір порядку файлів під час запуску для обробки декількох файлів HTML, щоб дізнатися більше.

Автоматичне створення списку змісту

Нарешті, спробуймо щось трохи амбіційніше. Припустімо, що у вашій книзі є заголовки у теґах h1 і h2, які виглядають так: <h1 id="якийсь_ідентифікатор">Якийсь Текст</h1>. Ми автоматично створимо таблицю змісту HTML на основі цих заголовків. Створіть таку нетипову функцію:

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'

І скористайтеся цим у виразі для пошуку:

<(h[12]) [^<>]* id=['"]([^'"]+)['"][^<>]*>([^<>]+)

Виконайте пошук у Всіх текстових файлах. Наприкінці пошуку буде показано контекстне вікно «Діагностичні повідомлення від вашої функції», у якому буде показано таблицю змісту у форматі HTML, готову до вставлення до toc.html.

Наведений вище код функції містить багато коментарів, тому його доволі просто зрозуміти. Ключовою новою можливістю є використання ще одного додаткового аргументу функції replace(), а саме, об’єкта data. Об’єкт data є словником Python, який зберігається протягом усіх послідовних викликів replace() під час виконання однієї дії Замінити всі.

Ще однією новою можливістю є використання call_after_last_match — встановлення для цієї змінної значення True у функції replace() означає, що редактор викликатиме replace() додатковий раз після знайдення усіх відповідників. Для цього додаткового виклику об’єктом-відповідником буде None.

Тут наведено лише приклади потужності функціонального режиму. Якщо вам дійсно потрібно створити таблицю змісту на основі заголовків у вашій книзі, вам варто скористатися спеціалізованим інструментом для створення таблиць змісту, а саме, пунктом меню Інструменти  →  Зміст.

Програмний інтерфейс функціонального режиму

Усі функції режиму функцій мають бути функціями Python із назвою replace, з таким підписом:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
return a_string

Під час виконання дії із пошуку з заміною для кожного знайденого відповідника викликатиметься функція replace() і має бути повернуто замінений рядок для кожного відповідника. Якщо замін не виконуватиметься, має бути повернуто match.group(), яким є початковий рядок. Документацію з різноманітних аргументів функції replace() наведено нижче.

Аргумент «match»

Аргумент match представляє поточний знайдений відповідник. Це об’єкт Match Python [https://docs.python.org/library/re.html#match-objects]. Його найкориснішим методом є group(), метод, яким можна скористатися для отримання відповідного тексту для окремих груп захоплення під час пошуку за формальним виразом.

Аргумент «number»

Аргумент number є номером поточного відповідника. Коли ви наказуєте програмі Замінити усе, виявлення кожного наступного відповідника призводитиме до того, що replace() викликатиметься зі збільшеним числом. Перший відповідник матиме номер 1.

Аргумент «file_name»

Це назва файла, у якому було знайдено поточний відповідник. Якщо пошук виконується у позначеному фрагменті тексту, значення file_name є порожнім. Значення file_name записується у канонічній формі, як шлях відносно кореневої теки книги. з використанням роздільника елементів шляху /.

Аргумент «metadata»

Є метаданими поточної книги, зокрема даними щодо назви, авторів, мови тощо. Є об’єктом класу calibre.ebooks.metadata.book.base.Metadata. Серед особливо корисних атрибутів, title (назва або заголовок книги), authors (список авторів) та language (код мови).

Аргумент «dictionaries»

Збірка словників, які використовуються для перевірки правопису у поточній книзі. Найкориснішим методом є dictionaries.recognized(слово). Цей метод повертає True, якщо слово пройшло перевірку за словником для мови поточної книги.

Аргумент «data»

Це простий словник Python. Після натискання кнопки Замінити всі кожен наступний відповідник спричинятиме виклик replace() із тим самим словником як даними. Таким чином, ви можете скористатися ним для збереження довільних даних між викликами replace() під час виконання дії Замінити всі.

Аргумент «functions»

Аргумент functions надає вам доступ до усіх інших визначених користувачем функцій. Корисний для повторного використання коду. Ви можете визначити допоміжні функції у одному місці, а потім скористатися ними у всіх інших ваших функціях. Наприклад, припустімо, що ви створили таку функцію із назвою My Function:

def utility():
# do something

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

Далі, у іншій функції, ви можете отримати доступ до функції utility() ось так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
utility = functions['My Function']['utility']
...

Ви також можете скористатися об’єктом functions для зберігання даних, які може бути повторно використано іншими функціями. Наприклад, ви можете створити функцію, яка під час запуску за допомогою кнопки Замінити всі збирає певні дані, і іншу функцію, яка використовує ці зібрані дані під час запуску. Розгляньмо такі дві функції:

# 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']
...

Діагностика ваших функцій

Ви можете виконувати діагностику створених вами функцій за допомогою стандартної функції print() Python. Виведені цією функцією дані буде показано у контекстному вікні після завершення дії з пошуку з заміною. Вище було наведено приклад використання print() для виведення цілої таблиці змісту.

Вибір порядку файлів під час запуску для обробки декількох файлів HTML

Якщо ви виконуєте дію Замінити всі для декількох файлів HTML одночасно, порядок, у якому оброблятимуться файли, залежить від того, які з файлів було відкрито для редагування. Ви можете наказати обробляти файли у порядку, у якому вони з’являються у книзі, встановленням атрибута file_order для вашої функції, ось так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.file_order = 'spine'

file_order приймає два значення, spine і spine-reverse, які спричиняють обробку декількох файлів у порядку, у якому вони зустрічаються у книзі, послідовно вниз або вгору списком, відповідно.

Як зробити так, що вашу функцію було викликано ще раз після виявлення останнього відповідника

Іноді, як у прикладі із автоматичним створенням таблиці змісту, який наведено вище, корисно зробити так, щоб вашу функцію було викликано ще раз після виявлення останнього відповідника. Зробити це можна встановленням для вашої функції атрибута call_after_last_match, ось так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.call_after_last_match = True

Дописування виведеного функцією до позначеного тексту

Якщо виконується пошук з заміною для позначеного фрагмента тексту, іноді корисно дописувати текст наприкінці позначеного фрагмента. Зробити це можна встановленням для вашої функції атрибута append_final_output_to_marked (зауважте, що також слід встановити call_after_last_match), ось так:

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

Придушення діалогового вікна результатів під час виконання пошуків у позначеному тексті

Крім того, ви можете придушити появу діалогового вікна результатів (що може уповільнити регулярне застосування пошуку з заміною до багатьох блоків тексту) встановивши для вашої функції атрибут suppress_result_dialog, ось так:

def replace(match, number, file_name, metadata, dictionaries, data, functions, *args, **kwargs):
...

replace.suppress_result_dialog = True

Додаткові приклади

Додаткові корисні приклади, авторами яких є користувачі calibre, можна знайти на форумі редактора книг calibre [https://www.mobileread.com/forums/showthread.php?t=237181].

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Редагування електронних книг »
- Фрагменти

Фрагменти

У редакторі книг calibre передбачено підтримку фрагментів. Фрагмент — це шматок тексту, який часто використовується повторно або містить багато зайвого тексту. Редактор надає вам змогу вставляти фрагмент за допомогою натискання певної комбінації клавіш. Наприклад, припустімо, що вам часто доводиться вставляти теґи, коли ви редагуєте файли HTML. Тоді, ви можете просто ввести <a у редакторі і натиснути Control+J. Редактор розгорне це до такого виразу:

<a href="filename"></a>

Крім того, буде позначено слово filename, а курсор розташовано над цим словом. Отже, ви зможете одразу почати введення справжньої назви файла, використовуючи чудову можливість Автоматичне доповнення, яку передбачено у редакторі. Щойно ви завершите введення назви файла, натисніть Control+J ще раз, щоб перевести курсор на позицію між теґами <a> і почати введення тексту для посилання.

Система фрагментів у редакторі є дуже потужною: передбачено декілька вбудованих фрагментів, ви можете створювати власні фрагменти, які відповідають вашому стилю редагування.

Наведене нижче обговорення вбудованих фрагментів має допомогти вам зрозуміти потужність системи фрагментів.

Примітка

Ви також можете використовувати фрагменти у полях для введення тексту на панелі пошуку з заміною. Втім, замінники (з використанням Control+J для переходу) не працюватимуть.

Вбудовані фрагменти

Нижче наведено опис вбудованих фрагментів. Зауважте, що ви можете перевизначити ці фрагменти вашими власними фрагментами із тим самим текстом-вмикачем.

Вставляння тексту-заповнювача [Lorem]

Перший і найпростіший із вбудованих фрагментів призначено для вставляння тексту-заповнювача до документа. Текст-заповнювач запозичено з De finibus bonorum et malorum [https://en.wikipedia.org/wiki/De_finibus_bonorum_et_malorum], філософської роботи Цицерона (перекладеної англійською). Для того, щоб скористатися цим фрагментом, просто введіть Lorem у файлі HTML і натисніть Control+J. Введене слово буде замінено абзацами тексту-заповнювача.

Визначення цього фрагмента є дуже простим: текст-перемикач визначено як Lorem, шаблон визначено як простий текст, який слід вставити. Вам буде дуже просто налаштувати цей фрагмент відповідно до вашої улюбленої форми тексту-заповнювача.

Вставляння закритого теґу HTML [<>]

Тепер, розгляньмо простий приклад потужної концепції замінників. Припустімо, ви хочете вставити самозакривний теґ <hr/>. Просто введіть <> і натисніть Control+J. Редактор розгорне цей фрагмент до такого тексту:

<|/>

Тут символ | відповідає поточному розташуванню курсора. Далі, ви можете ввести hr і натиснути Control+J, щоб пересунути курсор за кінець теґу. Цей фрагмент визначено ось так:

Trigger: <>
Template: <$1/>$2

Замінниками є комбінації символу долара ($) із цифрами. Після розгортання фрагмента у відповідь на натискання Control+J курсор спочатку буде розташовано на місці першого замінника (замінника із найменшим номером). Після повторного натискання Control+J курсор буде пересунуто до наступного замінника (замінника із наступним найменшим номером).

Вставити HTML теґ посилання [<a]

Усі теґи посилань у HTML мають однакову структуру. У них міститься атрибут href і якийсь текст між початковим і кінцевим теґами. Фрагмент для ефективного введення теґів посилань познайомить нас із додатковими можливостями замінників. Щоб скористатися цим фрагментом, просто введіть <a і натисніть Control+J. Редактор розгорне фрагмент до такого:

<a href="filename|"></a>

Крім того, буде позначено слово filename, а курсор розташовано над цим словом. Отже, ви зможете одразу почати введення справжньої назви файла, використовуючи чудову можливість Автоматичне доповнення, яку передбачено у редакторі. Щойно ви завершите введення назви файла, натисніть Control+J ще раз, щоб перевести курсор на позицію між теґами <a> і почати введення тексту для посилання. Після завершення введення тексту натисніть клавішу Control+J ще раз, щоб перейти до позиції після кінцевого теґу. Цей фрагмент визначено ось так:

Trigger: <a
Template: <a href="${1:filename}">${2*}</a>$3

Тут ми бачимо ще декілька нових можливостей. По-перше, замінник $1 став складнішим. Тепер він включає деякий типовий текст (слово filename). Якщо замінник містить типовий текст, замінник буде заміщено типовим текстом під час розгортання фрагмента. Крім того, під час переходів до замінника із типовим текстом за допомогою натискання клавіші Control+J програма автоматично позначатиме типовий текст. Таким чином, ви можете скористатися типовим текстом як нагадуванням про те, що вам слід заповнити певні важливі місця у шаблоні. Типовий текст для замінника можна вказати за допомогою такої синтаксичної конструкції: ${<число>:типовий текст}.

Ще однією новою можливістю є те, що у другому заміннику використано зірочку після цифри (${2*}). Цей означає, що замінник у шаблоні буде замінено на будь-який текст, який було позначено до розгортання шаблона. Щоб побачити, як це працює, позначте якийсь фрагмент тексту у редакторі, натисніть клавішу Control+J, введіть <a і натисніть клавішу Control+J ще раз. Шаблон буде розгорнуто до такого рядка:

<a href="filename">whatever text you selected</a>

Вставити HTML теґ зображення [<i]

Цей фрагмент дуже подібний до фрагмента для вставляння посилання HTML, який ми обговорювали вище. Він надає вам змогу швидко ввести теґ <img src="filename" alt="description" /> і здійснювати переходи між атрибутами src і alt:

Trigger: <i
Template: <img src="${1:filename}" alt="${2*:description}" />$3

Вставити довільний теґ HTML [<<]

Це надасть вам змогу вставити довільний повний теґ HTML (або загорнути позначений текст у теґ). Щоб скористатися цим, просто введіть << і натисніть Control+J. Редактор розгорне це до такого виразу:

<|></>

Введіть назву теґу, наприклад span, і натисніть Control+J. Результат буде таким:

<span>|</span>

Як бачите, кінцевий теґ автоматично заповнено рядком span. Цього вдалося досягти за допомогою ще однієї можливості замінників — віддзеркалення. Віддзеркалення працює так: якщо ви використали замінник у шаблоні декілька разів, другу і усі інші позиції буде автоматично заповнено тим, що ви введете на першій позиції, щойно буде натиснуто клавішу Control+J. Цей фрагмент визначено ось так:

Trigger: <<
Template: <$1>${2*}</$1>$3

Як ви бачите, перший замінник ($1) було вказано двічі, другий раз у завершальному тезі, де він просто скопіює усе, що ви ввели у початковому тезі.

Вставляння довільного теґу HTML із атрибутом класу [<c]

Це дуже схоже на наведений вище приклад вставляння довільного теґу, але тут потрібно вказати клас теґу:

Trigger: <c
Template: <$1 class="${2:classname}">${3*}</$1>$4

Ви зможете спочатку набрати назву теґу, натиснути Control+J, ввести назву класу, натиснути Control+J, ввести вміст теґу і натиснути Control+J востаннє, щоб вийти з теґу. Завершальний теґ буде додано автоматично.

Створення ваших власних фрагментів

Фрагменти є дуже корисним елементом редактора, оскільки ви можете створювати власні фрагменти, які відповідають вашому стилю редагування. Для створення фрагментів послідовно скористайтеся пунктами Зміни  →  Налаштування  →  Параметри редактора  →  Керування фрагментами у вікні редактора книг. У відповідь буде відкрито просте у користуванні діалогове вікно, яке допоможе вам у створенні ваших фрагментів. Натисніть кнопку Додати фрагмент, і програма відкриє ось таке діалогове вікно:

Спочатку надайте вашому фрагменту назву. Назва має бути описовою, такою, яка допоможе вам шукати ваш фрагмент. Далі вкажіть перемикач. Перемикачем є текст, який слід набрати у редакторі до натискання Control+J, щоб програма розгорнула його до вашого фрагмента.

Далі вкажіть шаблон фрагмента. Вам слід почати з одного з наведених вище прикладів і внести до нього зміни відповідно до ваших потреб. Нарешті, вкажіть, для яких типів файлів слід активізувати цей фрагмент. Таким чином, ви можете мати декілька фрагментів із однаковими назвами, які будуть працювати по-різному у файлах різних типів.

Наступним кроком є тестування вашого щойно створеного фрагмента. Для тестування скористайтеся полем Перевірка, яке розташовано у нижній частині вікна. Введіть текст-перемикач і натисніть Control+J, щоб розгорнути фрагмент і перейти між замінниками.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Сервер даних calibre

Сервер даних calibre

За допомогою Сервера даних calibre ви можете отримувати доступ до ваших бібліотек calibre і читати книги безпосередньо у програмі для перегляду інтернету на вашому улюбленому мобільному телефоні або планшетному пристрої. Таким чином, у вас не буде потреби у встановленні спеціальних програм для читання і керування книгами на вашому телефоні. Достатньо буде програми для перегляду інтернету. Сервер отримує і зберігає книгу, яку ви читаєте у автономному кеші, отже ви зможете продовжити читання, навіть якщо з’єднання з інтернетом буде втрачено.

Зміст

- Доступ до сервера даних з інших пристроїв

- Доступ до сервера з пристроїв у вашій домашній мережі

- Доступ до сервера з довільного комп’ютера у інтернеті

- Інтерфейс сервера

- Список книг

- Засіб для читання книг

- Підтримка з боку програм для перегляду інтернету

- Вмикання підтримки автономної роботи

- Керування обліковими записами лише з командного рядка

- Інтегрування сервера даних calibre в інші сервери

- Використання повного віртуального вузла

- Використання префікса адреси

- Створення служби для сервера calibre у сучасній системі Linux

Щоб запустити сервер, натисніть кнопку З’єднатись/Роздати і виберіть пункт Запустити сервер даних. Можливо, вам буде показано повідомлення від брандмауера або антивірусної програми на вашому комп’ютері щодо надання доступу до мережі програмі calibre.exe. Натисніть кнопку Дозволити або Гаразд. Далі, відкрийте програму для перегляду інтернету (бажано Chrome або Firefox) на вашому комп’ютері і введіть таку адресу:

http://127.0.0.1:8080

У відповідь у програмі для перегляду інтернету буде відкрито сторінку зі списком ваших бібліотек calibre. Натисніть пункт однієї з бібліотек і перегляньте список книг у бібліотеці. У відповідь на натискання пункту книги ви зможете переглянути метадані щодо неї та отримати доступ до кнопок Читати і Отримати. Щоб розпочати читання книги, натисніть кнопку Читати.

Примітка

Вказаною вище адресою, http://127.0.0.1:8080, можна скористатися лише на тому комп’ютері, де запущено calibre. Щоб отримати доступ до сервера з іншого комп’ютера, телефону, планшета тощо, вам доведеться ще дещо налаштувати. Докладніше про це у наступному розділі.

Доступ до сервера даних з інших пристроїв

Існує два типи віддаленого доступу з пристроїв, які, типово, можуть вам знадобитися. Перший, простіший, — це доступ з вашої домашньої мережі. Якщо calibre працює на комп’ютері у вашій домашній мережі, і у вас вже є інші пристрої, з’єднані із цією домашньою мережею, вам буде просто отримати доступ до сервера з цих пристроїв.

Доступ до сервера з пристроїв у вашій домашній мережі

Після запуску сервера calibre у описаний вище спосіб натисніть кнопку З’єднатись/Роздати ще раз. Замість пункту Запустити сервер даних має бути показано пункт Зупинити сервер даних. Праворуч від пункту цієї дії буде показано IP-адресу і номер порту. Це виглядає як послідовність чисел, відокремлених крапками. Приклад:

Stop Content server [192.168.1.5, port 8080]

Ці числа підкажуть вам, за якою адресою слід встановлювати з’єднання із сервером з ваших пристроїв. Якщо використовувати наведених вище приклад, адресою буде:

http://192.168.1.5:8080

Першою частиною адреси завжди є рядок http://, далі йде IP-адреса, тобто числа до коми, і нарешті, номер порту, який слід додати до IP-адрес після двокрапки (:). Якщо інших проблем немає, цього достатньо, і ви зможете переглядати бібліотеки calibre за допомогою вашого пристрою. Якщо ж нічого не виходить, доведеться читати цей підручник далі.

Діагностика з’єднання у домашній мережі

Якщо ви не можете встановити з’єднання із сервером за допомогою вашого пристрою, спробуйте виконати такі кроки:

1. Переконайтеся, що сервер працює, відкривши сторінку з адресою http://127.0.0.1:8080 у вашій програмі для перегляду інтернету на комп’ютері, на якому запущено сервер.

2. Перевірте, чи дозволено у налаштуваннях вашого брандмауера або антивіруса з’єднання з вашим комп’ютером на порту 8080 для програми calibre. Найпростішим способом усунути джерело проблеми, якщо проблему пов’язано із брандмауером або антивірусом, є тимчасове вимикання обох цих засобів захисту із наступною спробою встановити з’єднання. Щоб захистити ваш комп’ютер від загроз з інтернету, перш ніж вимикати брандмауер, від’єднайте комп’ютер від мережі.

3. Переконайтеся, що ваш пристрій і комп’ютер працюють у одній мережі. Це означає, що обидва пристрої має бути з’єднано із одним бездротовим маршрутизатором. Зокрема, жоден з пристроїв не повинен використовувати стільникове з’єднання або надане оператором безпосереднє з’єднання WiFi.

4. Якщо використано нестандартні налаштування мережі, адреса, показана у меню З’єднатись/Роздати, може бути помилковою. У такому випадку вам доведеться визначити правильну IP-адресу власноруч. На жаль, оскільки можливих налаштувань мережі дуже багато, ми не можемо дати конкретних порад щодо визначення адреси.

5. Якщо у вашій конфігурації для доступу потрібно вказати ім’я користувача і пароль, спочатку спробуйте з’єднатися без них, щоб переконатися, що саме ім’я і пароль спричиняють проблеми. На деяких пристроях з електронним чорнилом у програмах для перегляду інтернету просто не передбачено можливість обробки розпізнавання за іменем і паролем. Іноді, можна скористатися обхідним маневром, включивши ім’я і пароль до самої адреси. Приклад: http://користувач:пароль@192.168.1.2:8080.

6. Якщо у вас нічого не виходить, ви завжди можете попросити про допомогу на форумі користувачів calibre [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Доступ до сервера з довільного комп’ютера у інтернеті

Попередження

Перш ніж це робити, вам слід увімкнути захист розпізнаванням користувача з паролем на сервері. Якщо ви цього не зробите, доступ до ваших книг отримає будь-хто. Відкрийте сторінку Налаштування  →  Спільний доступ  →  Обмін мережею і позначте пункт Вимагати введення імені користувача і пароля для доступу до сервера даних.

Хоча окремі речі у налаштовуванні доступу до інтернету залежать від налаштувань мережі і типу використаного комп’ютера, загальну схему можна знайти нижче.

1. Визначте зовнішню IP-адресу комп’ютера, на якому ви збираєтеся запустити сервер. Зробити це можна за допомогою сайта What is my IP address [https://www.whatismyip.com/], відкривши його сторінку у програмі для перегляду інтернету на відповідному комп’ютері.

2. Якщо комп’ютер працює з інтернетом за допомогою маршрутизатора, увімкніть на маршрутизаторі переспрямовування портів, щоб переспрямувати порт 8080 (або інший вибраний вами порт для запуску сервера даних calibre) на комп’ютер.

3. Переконайтеся, що доступ до сервера calibre уможливлено у налаштуваннях брандмауерів та антивірусних програм, які працюють на вашому комп’ютері.

4. Тепер ви повинні мати змогу отримати доступ до сервера з будь-якого пристрою, з’єднаного з інтернетом, вказавши IP-адресу, яку ви визначили на першому кроці. Наприклад, якщо визначена вами IP-адреса — 123.123.123.123, а порт, яким ви користуєтеся на сервері calibre — 8080, адресою, яку слід ввести на пристрої, буде така: http://123.123.123.123:8080.

5. Ви також можете скористатися службою, подібною до no-ip [https://www.noip.com/free], для визначення простої для запам’ятовування адреси замість цифрової IP-адреси, яку ви визначити на першому кроці.

Примітка

Щоб забезпечити максимальний захист, вам слід також увімкнути на сервері даних HTTPS. Зробити це можна або на самому сервері, вказавши шлях до сертифіката HTTPS, яким слід користуватися, у параметрах додаткового налаштовування для сервера, або за допомогою зворотного проксі, налаштовування якого описано нижче, якщо ви хочете скористатися наявним налаштуванням HTTPS.

Інтерфейс сервера

Інтерфейс сервера є спрощеною версією основного інтерфейсу calibre, яку оптимізовано для сенсорних екранів. На домашній сторінці вам буде показано книги, які ви читаєте, а також запропоновано вибрати бібліотеку calibre, книги у якій ви хочете переглянути. Сервер у calibre надає вам доступу до усіх ваших бібліотек, а не лише однієї, як це було у попередніх версіях.

Список книг

Список книг на сторінці сервера це проста таблиця із зображеннями обкладинок. Натисніть обкладинку, щоб переглянути докладні метадані щодо книги або щоб розпочати читання книги. Якщо вам потрібен список із докладнішими даними, ви можете змінити типовий режим перегляду, натиснувши кнопку із трьома вертикальними крапками, яку розташовано у правому верхньому куті екрана.

Прийоми упорядковування та пошуку книг мають бути знайомими користувачам calibre. Доступ до відповідних можливостей можна здійснювати за допомогою натискання піктограм у верхній правій частині екрана. Обидві можливості працюють абсолютно так само, як і у основній програмі calibre. На сторінці пошуку ви навіть можете будувати запити, натискаючи пункти авторів, міток тощо, зовсім так само, як ви це робите на панелі перегляду міток у вікні основної програми.

Одна із найулюбленіших можливостей основної програми, Віртуальні бібліотеки, також є частиною інтерфейсу сервера. Натисніть кнопку із трьома вертикальними крапками у верхній правій частині екрана, щоб вибрати віртуальну бібліотеку.

Засіб для читання книг

Ви можете почати читати будь-яку книгу із вашої бібліотеки calibre простим натисканням її пункту з наступним натисканням кнопки Читати. Працювати із засобом читання книг дуже просто. Гортати сторінки можна як натисканням, так і рухом пальця. Рухами пальцем вгору-вниз можна переходити між розділами книги. Натисканням у верхній чверті екрана ви можете отримати доступу до засобів керування програмою та параметрів перегляду.

Якщо сервер даних постійно працюватиме, ви можете навіть відкривати книгу на різних пристроях, зберігаючи при цьому місце, на якому ви закінчили читання попереднього разу. Якщо синхронізація місця читання не відбувається, ви можете виконати її у примусовому режимі. Для цього натисніть на екран у верхній чверті і виберіть у меню пункт Синхронізувати.

Підтримка з боку програм для перегляду інтернету

У новому сервері calibre широко використано додаткові можливості HTML 5 і CSS 3. Через це, для його роботи потрібна сучасна програма для перегляду інтернету. Тестування виконувалося на Android Chrome та iOS Safari, а також на Chrome і Firefox на комп’ютері.

У сервері використовуються лише ті можливості, які вже стандартизовано, або ті, які буде стандартизовано найближчим часом. Тому, якщо сервер не працює із вашою улюбленою програмою для перегляду інтернету, він запрацює у якійсь найближчій версії вашої програми.

Якщо ви користуєтеся застарілою програмою для перегляду інтернету, програмою із обмеженими можливостями або не хочете користуватися JavaScript, ви можете скористатися мобільним режимом перегляду, просто додавши /mobile до адреси сервера.

Примітка

У iOS Apple надає змогу використовувати лише один рушій роботи браузера. Отже, Firefox, Chrome і Safari насправді є одним і тим самим браузером. Для роботи нового інтерфейсу сервера потрібна iOS 10.3.2 або новіша версія. На Android сервер працює з Chrome версії 58 або новішої.

Вмикання підтримки автономної роботи

Розробники браузерів намагаються спонукати користувачів до вживання SSL, вимикаючи додаткові можливості браузерів для простих (нешифрованих) з’єднань HTTP. Однією із таких додаткових можливостей є ApplicationCache, те, що у calibre було використано для підтримки автономної роботи. У результаті на сьогодні, на жаль, програма може працювати в автономному режимі, лише якщо ви не закриєте вкладку браузера. Крім того, у Firefox для Android вам доведеться ввести адресу about:config і створити параметр із назвою browser.tabs.useCache, встановивши для нього значення true.

Керування обліковими записами лише з командного рядка

У програмі calibre є чудовий розділ у вікні Налаштування, за допомогою якого ви можете керувати обліковими записами для сервера. Втім, якщо ви хочете користуватися лише сервером і не маєте змоги запустити основну програму calibre на тому самому комп’ютері або обліковому записі, ви можете керувати записами користувачів за допомогою лише інтерфейсу командного рядка.

Щоб перейти до режиму керування записами користувачів, скористайтеся параметром --manage-users під час запуску окремої програми calibre-server. Припустімо, вам потрібно зберегти базу даних користувачів до теки /srv/calibre. Створіть її за допомогою такої команди:

calibre-server --userdb /srv/calibre/users.sqlite --manage-users

Виконайте настанови щодо створення облікових записів користувачів, встановлення паролів тощо. Виконавши відповідні дії, запустіть сервер такою командою:

calibre-server --userdb /srv/calibre/users.sqlite --enable-auth

На запущеному сервері використовуватимуться облікові записи користувачів, створені вами на попередньому кроці.

Інтегрування сервера даних calibre в інші сервери

Тут описано спосіб інтеграції сервера даних calibre до іншого сервера. Найпоширенішою причиною для такої інтеграції є використання SSL або обслуговування бібліотеки calibre як частини більшого сайта. Основною методикою є запуск сервера calibre і налаштовування на основному сервері зворотного проксі-сервера до сервера даних.

Зворотною називається схема проксі-сервера, коли ваш звичайний сервер приймає вхідні запити і передає їх серверу calibre. Далі він читає відповідь від сервера calibre і переспрямовує її клієнту. Це означає, що ви можете просто запустити сервер calibre як звичайно, без спроб тісно інтегрувати його зі вашим основним сервером.

Використання повного віртуального вузла

Найпростішою є конфігурація із використанням повного віртуального вузла для роботи сервера calibre. У цьому випадку сервер calibre запускається так:

calibre-server

Далі, слід налаштувати віртуальний вузол на вашому основному сервері. Приклад для 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;
}
}

Або для 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>

Використання префікса адреси

Якщо ви не хочете використовувати окремий віртуальний вузол для calibre, ви можете налаштувати систему з використанням префікса адреси. Запустіть сервер calibre так:

calibre-server --url-prefix /calibre --port 8080

Ключовим параметром тут є --url-prefix /calibre. Цей параметр наказує серверу даних обслуговувати усі адреси із префіксом calibre. Щоб побачити як це праціює, відкрийте сторінку з адресою http://localhost:8080/calibre у вашій програмі для перегляду інтернету. Ви маєте побачити звичайний сайт сервера даних, але тепер він працюватиме у теці «/calibre».

Для nginx потрібні такі налаштування:

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

Для Apache спочатку увімкніть модулі проксі в Apache додаванням таких рядків до httpd.conf:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

Точна методика вмикання модулів проксі залежить від встановленого варіанта Apache. Щойно модулі проксі буде увімкнено, додайте такі правила до файла :file»httpd.conf (або якщо ви використовуєте віртуальні вузли, до файла налаштувань віртуального вузла):

AllowEncodedSlashes On
RewriteEngine on
RewriteRule ^/calibre/(.*) http://127.0.0.1:8080/calibre/$1 [proxy]
RedirectMatch permanent ^/calibre$ /calibre/

Ось і усе, тепер ви можете отримувати доступ до сервера даних calibre за префіксом «/calibre» на вашому основному сервері. Вказані вище правила передають усі запити до «/calibre» на сервер calibre, який запущено на порту 8080 і, завдяки вказаному параметру «–url-prefix», сервер calibre прозоро їх обробляє.

Примітка

Якщо використовується зворотний проксі, вам слід повідомити сервер даних calibre про те, що слід очікувати на встановлення зв’язку лише від локального вузла, скориставшись параметром --listen-on 127.0.0.1. Таким чином, сервер оброблятиме лише ті з’єднання, які встановлюватиметься з того самого комп’ютера, на якому його запущено, тобто з’єднання від зворотного проксі.

Примітка

Якщо вами налаштовано SSL на вашому основному сервері, вам слід повідомити сервер calibre про те, що слід використовувати базове розпізнавання замість розпізнавання за контрольними сумами, оскільки воно працює швидше. Для цього передайте параметр --auth-mode=basic програмі calibre-server.

Створення служби для сервера calibre у сучасній системі Linux

Створити службу для запуску calibre під час завантаження у сучасній заснованій на systemd [https://www.freedesktop.org/wiki/Software/systemd/] системі Linux дуже просто. Створіть файл /etc/systemd/system/calibre-server.service із таким вмістом:

[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

Замініть mylinuxuser і mylinuxgroup на користувача і групу, від імені яких має бути запущено сервер. Користувач і група мають бути тими самими, що і користувач і група власника файлів у теці бібліотеки calibre. Зауважте, що, зазвичай, не варто запускати сервер від імені адміністратора (root). Крім того, вам слід змінити шлях до теки бібліотеки calibre так, щоб він збігався зі шляхом у вашій системі. Якщо треба, можете додати декілька бібліотек. Докладніший опис можна знайти у довідці до програми calibre-server.

Тепер віддайте команду:

sudo systemctl start calibre-server

для запуску сервера. Перевірте стан сервера такою командою:

sudo systemctl status calibre-server

Щоб сервер запускався під час завантаження системи, віддайте таку команду:

sudo systemctl enable calibre-server

Примітка

Для роботи сервера calibre не потрібен графічний серве X, але йому знадобляться встановлені бібліотеки X, оскільки деякі з його компонентів зібрано із використанням цих бібліотек.

Примітка

У сервері calibre також передбачено підтримку активації сокетів systemd. Отже, ви можете нею скористатися, якщо цього захочете.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Порівняння електронних книг

Порівняння електронних книг

До calibre вбудовано засіб порівняння електронних книг, за допомогою якого можна переглядати відмінності між версіями книги та виконувати перетворення. За допомогою цього засобу можна порівнювати книги у форматах EPUB та AZW3.

Щоб порівняти книги, відкрийте редактор книг натиснувши Редагування електронних книг та натисніть Файл  →  Порівняти з іншою книгою, або скористайтеся панеллю Подробиці щодо книги. Якщо зробити перетворення з формату EPUB на формат EPUB, початковий файл буде збережено як ORIGINAL_EPUB. Просто натисніть правою кнопкою миші на ORIGINAL_EPUB на панелі «Подробиці щодо книги» і виберіть Порівняти з форматом EPUB.

Вікно засобу порівняння буде подібним до показаного на знімку вище. У ньому ви побачите відмінності у тексті, стилях та зображеннях у вибраних вами книгах.

Опис панелі порівняння

На зображеннях вище, можна побачити, як панель порівняння показує відмінності двох книг, розташованих поруч. Разом з відмінностями показані лише декілька рядків контексту. Таким чином, одного погляду достатньо, щоб продивитися зміни у великому документі, наприклад книзі.

Додані фрагменти буде показано із зеленим тлом, вилучені — із червоним, а змінені фрагменти — із синім тлом.

Номера рядків для тексту з розбіжностями показані по боках, що робить дуже зручним пересування до місця правки в редакторі. Якщо ви відкрили засіб порівняння книг в редакторі, подвійне клацання на рядку на панелі справа автоматично відкриє бажаний рядок у редакторі.

Якщо перед порівнянням книжок зробити попереднє покращення формату тексту та застосування стилів, то, зазвичай, результати порівняння можуть бути чистішими, а розбіжності більш очевидними. Для цього натисніть кнопку Параметри у правому нижньому куті і виберіть Форматувати файли до порівняння. Будь ласка, зауважте, що вибір цього варіанта може мати небажані наслідки, оскільки під час форматування програма спробує виправити помилки розмітки. Також, ви можете змінити кількість рядків контексту поруч з розбіжностям, натиснувши кнопку Параметри.

Ви можете здійснювати пошук тексту в розбіжностях через рядок пошуку внизу. Вам потрібно буде вказати, в межах якої панелі шукати, на Лівій чи на Правій.

Запуск засобу порівняння

Засіб порівняння найбільш корисний, коли у вас є дві версії одніє і тієї ж книги, і ви хочете побачити, які міжними відмінності. З цією метою існує кілька способів запуску засобу.

Порівняння двох файлів електронних книг

Відкрий те перший файл в редакторі книг Редагування електронних книг. Натисніть Файл  →  Порівняти з іншою книгою і виберіть інший файл (має бути такого саме формату, як і перший). Коли панель порівняння відкриється, ви побачите перший файл, що редагується, праворуч, а другий — ліворуч.

Порівняння ORIGINAL_FMT та FMT

Якщо перетворити книги за допомогою calibre без зміни формату FMT, то початковий файл буде збережено як ORIGINAL_FMT. Ви можете подивитися, що змінилося під час перетворення, натиснувши правою кнопкою миші на записі ORIGINAL_FMT на панелі Подробиці щодо книги головного вікна calibre та обравши Compare to FMT. Панель порівняння відкриється з ORIGINAL_FMT ліворуч, а FMT праворуч.

Порівняння контрольної точки з поточним станом книги під час редагування

Редактор Редагування електронних книг має дуже корисну функцію, що називається Контрольні точки. За допомогою цієї функції ви можете зберегти поточний стан книги як іменовану «контрольну точку», до якої ви зможете повернутися, якщо вам не подобаються зміни зроблені після неї. Контрольні позиції також створюються після виконання різноманітних автоматизованих дій в редакторі. Ви можете переглянути список контрольних позицій в Перегляд  →  Контрольні точки, а також порівняти поточний стан книги з обраною контрольною позицією. Контрольна точка на панелі порівняння показуватиметься ліворуч, а поточний стан — праворуч.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Редагування метаданих електронної книги

Редагування метаданих електронної книги

Зміст

- Редагування метаданих однієї книжки за раз

- Отримання метаданих

- Керування форматами книг

- Усе про обкладинки

- Одночасне редагування метаданих групи книг

- Пошук і заміна

- Пакетне отримання метаданих

- Додавання файлів даних до книги

Електронні книги дуже різнорідні за формою і розміром, метадані у них (записи про назву, автора, цикл та видавця) є неповними або неточними. Найпростішим способом внесення змін до метаданих у calibre є подвійне клацання на полі відповідного запису із наступним введенням правильних даних. Складніше, «потужне» редагування метаданих можна виконати за допомогою інструментів, описаних нижче.

Редагування метаданих однієї книжки за раз

Клацніть лівою кнопкою миші на пункті книги, яку ви хочете редагувати, а потім натисніть кнопку Редагувати метадані або клавішу E. У відповідь буде відкрито діалогове вікно, за допомогою якого ви зможете редагувати усі аспекти метаданих. У вікні передбачено різноманітні можливості для того, щоб зробити редагування швидшим і ефективнішим. Ось список підказок щодо цих можливостей:

- Ви можете натиснути кнопку між полями заголовка і авторів, щоб поміняти їх місцями автоматично.

- Ви можете натиснути кнопку поряд із кнопкою упорядковування за автором, щоб calibre автоматично заповнила відповідне поле на основі значень упорядковування, збережених для усіх авторів. Для внесення змін до значень упорядковування за авторами скористайтеся діалоговим вікном Керування записами авторів. Відкрити це діалогове вікно можна натисканням і утримуванням натиснутою кнопки, розташованої поряд із кнопкою упорядковування за авторами.

- Ви можете натиснути кнопку, розташовану поряд із мітками, щоб скористатися редактором міток для керування мітками, пов’язаними із книгою.

- Полем «Ідентифікатори» можна скористатися для введення ISBN (та багатьох інших типів ідентифікаторів). Поле буде мати червоне тло, якщо ви введете помилковий номер ISBN. Тло буде зеленим, якщо ISBN введено коректно.

- Поле упорядковування за авторами буде показано червоним кольором, якщо значення упорядковування за авторами буде відмінним від того, що очікується calibre.

Отримання метаданих

Найкращою можливістю діалогового вікна редагування метаданих є можливість автоматично заповнювати декілька полів метаданих, отримуючи дані з різних сайтів. У поточній версії calibre використовує Google Books і Amazon. Засіб отримання метаданих може заповнювати поля назви, автора, циклу, міток, оцінки, опису та ISBN.

Щоб отримати дані, заповніть поля назви і автора і натисніть кнопку Отримати метадані. calibre покаже вам список книг, які найточніше відповідають вказаним вами назві і автору. Якщо ви спочатку заповните поле ISBN, дані цього поля матимуть вищий пріоритет при пошуку за назву і автора. Якщо відповідників не буде знайдено, спробуйте зробити пошук менш специфічним, включивши до нього лише деякі ключові слова з назви і лише прізвище автора.

Керування форматами книг

У calibre із записом однієї книги може бути пов’язано декілька різних форматів. Наприклад, ви могли отримати повне зібрання творів Шекспіра у форматі EPUB, а пізніше перетворити його на файл у форматі MOBI для читання на пристрої Kindle. Керування форматами у calibre відбувається автоматично. У розділі Доступні формати діалогового вікна редагування метаданих буде показано список форматів. Ви можете додати новий формат, вилучити наявний формат, а також наказати calibre встановити метадані та зображення обкладинки для запису книги на основі метаданих з одного з форматів книги.

Усе про обкладинки

Ви можете наказати calibre отримати зображення обкладинки, якщо відомий номер ISBN. Крім того, ви можете вказати файл на вашому комп’ютері, яким слід скористатися як зображенням обкладинки. calibre навіть може створити типову обкладинку на основі базових метаданих книги. Ви можете перетягнути і скинути зображення на блок обкладинки, щоб змінити його, а також клацнути правою кнопкою миші на пункті зображення, щоб скопіювати його і вставити на місце обкладинки.

Крім того, передбачено кнопку для автоматичного обрізання полів на зображенні обкладинки. Ним можна скористатися, якщо на полях зображення обкладинки залишилися зайві фрагменти тла.

Одночасне редагування метаданих групи книг

Спочатку, позначте книги, які ви хочете редагувати. Для цього натисніть і утримуйте натиснутою клавішу Ctrl або Shift і клацайте лівою кнопкою миші на пунктах відповідних книг. Якщо ви виберете декілька книг, натискання кнопки Редагувати метадані призведе до відкриття нового діалогового вікна пакетного редагування метаданих. За допомогою цього вікна ви зможете швидко редагувати записи авторів, видавця, оцінки, міток, циклів декількох книг, встановлюючи для них однакові значення. Це зокрема корисно, якщо ви імпортували декілька книг, які мають спільні метадані. Діалогове вікно пакетного редагування є доволі потужним інструментом. Наприклад, у ньому передбачено вкладку «Пошук з заміною», за допомогою якої ви можете виконувати пакетні дії з метаданими і навіть копіювати метадані з одного стовпчика до іншого.

У звичайному діалоговому вікні редагування метаданих передбачено кнопки Далі і Назад, якими можна скористатися при одночасному послідовному редагуванні метаданих декількох книг.

Пошук і заміна

За допомогою діалогового вікна Редагувати метадані для багатьох книг ви можете виконувати над позначеними книгами довільні дії з пошуку із заміною метаданих. Типово використовується простий режим пошуку із заміною, але передбачено також підтримку формальних виразів. Докладніший опис формальних виразів наведено у розділі Все про використання формальних виразів в calibre.

Як зазначалося вище, передбачено два режими пошуку із заміною: режим посимвольної відповідності і режим формальних виразів. У режимі посимвольної відповідності програма виконуватиме пошук у вибраному вами Полі пошуку символів, які ви вкажете у полі Шукати, і замінить їх на символи, які ви вкажете у полі Замінити на. Замінено буде усі відповідники вказаних символів у полі. Наприклад, припустімо, що у полі, у якому відбувається пошук, міститься рядок котигорошко. Якщо відбуватиметься пошук о із заміною на ПРИВІТ, у результаті буде отримано кПРИВІТтигПРИВІТрПРИВІТшкПРИВІТ.

Якщо у полі, у якому відбувається пошук, міститься декілька значень, наприклад мітки, кожне значення буде оброблено окремо. Наприклад, якщо у ваших мітках міститься запис Жахи, Страшне, а виразом для пошуку є и,, нічого не буде замінено, оскільки спочатку пошук відбуватиметься у значення Жахи, а потім у значенні Страшне, які не містять шуканого рядка.

Якщо ви хочете, щоб під час пошуку програма не враховувала відмінностей між літерами верхнього і нижнього регістрів, зніміть позначку з пункту «З урахуванням регістру».

Ви можете наказати calibre змінити регістр символів результату (інформації після заміни), вибравши один із варіантів функцій у полі «Застосувати функцію після заміни». Доступні такі операції:

- Нижній регістр — замінити усі символи у полі на символи нижнього регістру.

- Верхній регістр — замінити усі символи у полі на символи верхнього регістру.

- Кожне Слово З Прописної — написати з великої літери кожне слово у результаті.

Передбачено поле Ваш тест, у якому ви можете ввести текстовий фрагмент для того, щоб перевірити, чи виконується пошук із заміною належним чином. Здебільшого, тестових полів книги має бути достатньо, але може трапитися так, що у вас виникне потреба перевірити щось, чого немає у цих полях. Тоді ви можете ввести ваш варіант рядка у поле Ваш тест.

У режимі формальних виразів пошук із заміною має певні відмінності від посимвольного режиму, окрім (звичайно ж) використання формальних виразів. Першою відмінністю є те, що функції застосовуються до частин рядка, що відповідає пошуковому рядку, а не до цілого поля. Другою відмінністю є те, що функції застосовуються до рядка заміни, а не до усього поля.

Третьою і найважливішою відмінністю є те, що рядок-замінник може посилатися на частини рядка пошуку за допомогою зворотних посилань. Зворотні посилання вказуються у форматі \\n, де n — ціле число, яке відповідає n-й групі у дужках у виразі для пошуку. Наприклад, якщо відбувається пошук у рядку Ми йдемо на бій, вказано вираз для пошуку Ми йдемо (…) (…) і вираз-замінник Ми йдемо \2 \1, результатом буде Ми йдемо бій на. Докладніший опис зворотних посилань можна знайти у розділі Все про використання формальних виразів в calibre.

Один із корисних прикладів: припустімо, що ви хочете змінити регістр усіх символів у полі. Найпростіше для цього буде скористатися посимвольним режимом, але нехай ви працюєте у режимі формальних виразів. Тоді рядком для пошуку має бути (^.*$), а рядком-замінником — \1, звичайно ж, із вибором бажаної функції зміни регістру символів.

Нарешті, у режимі формальних виразів ви можете копіювати значення з одного поля до іншого. Достатньо просто зробити поля джерела і призначення різними. Копія може замінити собою вміст поля призначення, її може бути дописано перед поточним вмістом цього поля (додано на початку) або дописано наприкінці поля (додано наприкінці). Скористайтеся полем „Використовувати кому“, щоб повідомити calibre, слід додати (або не додавати) кому між текстом і поточним вмістом поля призначення у режимах додавання на початку і наприкінці. Якщо поле призначення містить декілька записів (наприклад міток), ви не зможете зняти позначку з поля використання коми.

Пошук із заміною відбувається після того, як буде застосовано усі зміни у метаданих з інших вкладок. Це може трохи заплутувати користувача, оскільки текстові поля показуватимуть дані до інших змін, а сама дія відбуватиметься після інших змін. Якщо ви маєте якість сумніви щодо результату виконання дій, не поєднуйте пошук із заміною із внесенням інших змін.

Пакетне отримання метаданих

Якщо ви хочете отримати метадані для декількох книг одночасно, натисніть стрілочку праворуч від кнопки Редагувати метадані і виберіть у списку пункт Отримати метадані. Ви можете вибрати варіант отримання: отримати лише метадані, отримати лише зображення обкладинки або отримати і те, і інше.

Додавання файлів даних до книги
calibre може зберігати довільну кількість додаткових файлів, які пов’язано із книгою. Цими файлами можуть бути альтернативні обкладинки, додаткові матеріали тощо. Ці файли може бути переглянуто безпосередньо або використано як джерело перетворення. Їх не буде індексовано рушієм повнотекстового пошуку у calibre. Щоб переглянути, додати, вилучити їх, позначте книгу і клацніть правою кнопкою миші на кнопці Редагувати метадані і виберіть Керування файлами даних. У результаті буде відкрито контекстне вікно, за допомогою якого можна виконувати дії з цими файлами. Крім того, ви можете клацнути правою кнопкою миші на кнопці Додати книги і вибрати Додати файли даних до позначених записів книг, щоб пришвидшити додавання файлів даних.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Поширені питання та відповіді на них

Поширені питання та відповіді на них

Зміст

- Перетворення даних електронних книг у інші формати

- Інтеграція з пристроями

- Керування бібліотекою

- Різне

Перетворення даних електронних книг у інші формати

Зміст

- Підтримку перетворення яких форматів передбачено у calibre?

- Якими є найкращі початкові формати для перетворення?

- Перетворення файла PDF не дає бажаних наслідків. Що робити?

- Як перетворити файл, що містить нелатинські символи або теґи для позначення лапок?

- Що за проблеми зі списком змісту у файлах MOBI?

- Як перетворити збірку файлів HTML у певному порядку?

- Чому файл EPUB, який створено calibre, виявився некоректним?

- Як скористатися деякими додатковим можливостями інструментів перетворення?

Підтримку перетворення яких форматів передбачено у calibre?

У calibre передбачено підтримку багатьох форматів вхідних і вихідних даних. Програма здатна перетворити дані у будь-якому вхідному форматі з наведеного нижче списку на дані у будь-якому з вихідних форматів.

Формати вхідних даних: 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

Формати вихідних даних: AZW3, EPUB, DOCX, FB2, HTMLZ, OEB, LIT, LRF, MOBI, PDB, PMLZ, RB, PDF, RTF, SNB, TCR, TXT, TXTZ, ZIP

Примітка

PRC є загальною назвою формату. У calibre передбачено підтримку файлів PRC з заголовками TextRead та MOBIBook. PDB також є загальною назвою формату. У calibre передбачено підтримку файлів PDB у форматах eReader, Plucker (лише вхідні дані), PML та zTxt. Підтримка DJVU обмежується перетворенням файлів DJVU, які містять вбудований текстовий шар. Такі файли типово створюються програмами для оптичного розпізнавання текстів. Книги MOBI можуть належати до одного з двох типів: Mobi6 та KF8. У calibre передбачено повну підтримку обох цих типів. Файли MOBI часто мають суфікс назви .azw і .azw3. Передбачено підтримку файлів DOCX з версії Microsoft Word 2007 та новіших версій.

Якими є найкращі початкові формати для перетворення?

За зменшенням пріоритетності: LIT, MOBI, AZW, EPUB, AZW3, FB2, FBZ, DOCX, HTML, PRC, ODT, RTF, PDB, TXT, PDF

Перетворення файла PDF не дає бажаних наслідків. Що робити?

Дані файлів PDF перетворювати доволі важко. Список різноманітних проблем, пов’язаних із перетворенням PDF наведено тут: Перетворення документів PDF.

Як перетворити файл, що містить нелатинські символи або теґи для позначення лапок?

У цієї проблеми є два аспекти:

1. Дані щодо кодування вмісту початкового файла: calibre намагається визначити кодування даних ваших початкових файлів, але часто визначити кодування неможливо. Отже, вам доведеться повідомити програмі, яке кодування слід використовувати. Зробити це можна за допомогою графічного інтерфейсу, а саме поля Кодування вхідних даних на вкладці Вигляд та поведінка  →  Текст діалогового вікна перетворення. Для інструментів командного рядка передбачено параметр ebook-convert-txt-input --input-encoding.

2. Під час додавання файлів HTML до бібліотеки calibre, вам варто вказати calibre, яким є кодування тексту у файлах. Для цього скористайтеся пунктом Налаштування  →  Додатково -
> Додатки  →  Тип файла і налаштуйте додаток HTML у ZIP, повідомивши йому кодування, у якому зберігаються дані у ваших файлах HTML. Після цього calibre має належним чином обробляти ваші файли HTML. Кодування даних файлів HTML з різних джерел може бути різним, отже може виникнути потреба у коригування параметра кодування. Поширеним кодуванням багатьох файлів з інтернету є cp1252, тому рекомендуємо вам спробувати це кодування першим. Зауважте, що під час перетворення файлів HTML слід залишати згаданий вище параметр кодування порожнім. Причиною є те, що додаток HTML у ZIP автоматично перетворює дані файлів HTML у стандартне кодування (UTF-8).

Що за проблеми зі списком змісту у файлах MOBI?

Перше, що слід знати, є те, що у більшості книг є дві таблиці змісту. Однією з них є традиційна таблиця змісту, подібна до тієї, яку ви можете бачити у звичайних паперових книгах. Ця таблиця змісту є частиною основного документа, її може бути форматовано будь-яким чином за допомогою таблиці стилів. Цю таблицю змісту будемо називати змістом даних.

Крім того, є ще зміст метаданих. Зміст метаданих — таблиця змісту, яка не є частиною тексту книги і доступ до якої можна отримати за допомогою якоїсь спеціальної кнопки у програмі для читання. Наприклад, у програмі для читання книг calibre для перегляду цієї таблиці змісту використовують кнопку «Показати зміст». Стиль цієї таблиці змісту творець книги визначити не може. За її показ відповідає лише програма для перегляду книги.

Ситуація із форматом MOBI є заплутаною. Це спричинене тим, що у форматі MOBI, єдиному серед поширених форматів електронних книг, не передбачено підтримки змісту у метаданих. Книги MOBI імітують наявність змісту у метаданих, дописуючи додатковий список змісту у тексті наприкінці книги. Коли ви натискаєте кнопку переходу до змісту на вашому пристрої Kindle, пристрій Kindle переносить вас саме до цього додаткового розділу змісту.

Як бачите, у книзі MOBI є два ідентичних списки змісту. Слід пам’ятати, що один з них є семантично списком змісту у тексті, а інших — списком змісту у метаданих, хоча обидва містять одні й ті самі записи і виглядають однаково. Доступ до одного з них можна отримати безпосередньо з меню Kindle, інший же недоступний з цих меню.

Під час перетворення даних у формат MOBI calibre виявляє зміст у метаданих вхідного документа і створює зміст наприкінці тексту у виведеному файлі MOBI. Ви можете вимкнути його створення пунктом у параметрах виведення у MOBI. Ви також можете повідомити calibre, де слід розмістити список змісту у тексті, на початку чи наприкінці книги за допомогою іншого пункту у параметрах виведення у MOBI. Не забувайте, що цей список змісту семантично є змістом у метаданих, у будь-якому форматі, окрім MOBI, він не може бути частиною тексту. Те, що він є частиною тексту у MOBI просто прикра особливість, яку спричинено обмеженнями формату MOBI. Якщо вам потрібен список змісту у певному місці тексту документа, створіть його вручну. Отже, наполегливо рекомендуємо вам не змінювати типових налаштувань, тобто створення наприкінці книги списку змісту на основі змісту у метаданих. Крім того, зауважте, що якщо ви вимкнете створення списку змісту наприкінці тексту книги, отриманий файл MOBI може виявитися непридатним до читання на Kindle, оскільки на Kindle список змісту у метаданих використовується для багатьох речей, зокрема можливості гортання сторінок.

Якщо у документі вхідних даних список змісту створено вручну, ви можете скористатися параметрами виявлення списків змісту calibre для автоматичного створення змісту у метаданих на основі створеного вручну списку. Щоб дізнатися про те, як скористатися цими параметрами ознайомтеся із розділом щодо перетворення даних у підручнику користувача.

Нарешті, рекомендуємо вам позбутися списку змісту у тексті документа і зберігати для ваших електронних книг лише список змісту у метаданих. Списки змісту у метаданих надаватимуть читачам ваших електронних книг значно ширші можливості з навігації (окрім пристроїв Kindle, де вони збігатимуться із списками змісту у тексті книги).

Примітка

У новішому форматі AZW3 передбачено належну підтримку змісту у метаданих. Втім, відомо, що програми Kindle можуть обробляти файл не так, як слід, якщо ви вимкнете створення вбудованого списку вмісту наприкінці файла. Отже, рекомендуємо вам не вимикати створення такого змісту. Якщо ви створите файл AZW3 зі змістом у метаданих і без змісту наприкінці тексту, деякі можливості на Kindle не працюватимуть, зокрема можливість гортати сторінки.

Як перетворити збірку файлів HTML у певному порядку?

Щоб перетворити збірку файлів HTML у певному порядку, вам слід створити таблицю змісту файла. Тобто, слід створити ще один файл HTML, який міститиме посилання на усі інші файли у бажаному порядку. Вміст такого файла може бути подібним до наступного:

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

Далі, просто додайте цей файл HTML у графічному інтерфейсі редактора і скористайтеся кнопкою перетворення для створення книги. Ви можете скористатися пунктом у розділі таблиці змісту діалогового вікна перетворення для керування створенням таблиці змісту книги.

Примітка

Типово, під час додавання файлів HTML calibre проходить посилання у файлах у порядку спершу глибина. Це означає, що якщо файл A.html посилається на файл B.html, C.html і D.html, а файл B.html посилається на файл D.html, файли буде розташовано у такому порядку: A.html, B.html, D.html, C.html. Якщо ж вам потрібен порядок A.html, B.html, C.html, D.html, вам доведеться повідомити calibre, щоб програма додавала файли у порядку спершу шлях. Для цього відкрийте сторінку налаштувань Налаштування  →  Додатково  →  Додатки  →  Тип файла і налаштуйте додаток HTML у ZIP.

Чому файл EPUB, який створено calibre, виявився некоректним?

Calibre не гарантує, що створений за допомогою програми код EPUB буде коректним. Єдиною гарантією є передавання програмі коректного коду XHTML 1.1 + CSS 2.1. Тоді програма гарантовано створить коректний код EPUB. calibre докладає значних зусиль для того, щоб створений код EPUB працював як слід на широкому спектрі пристроїв. Ця мета є несумісною зі створенням коректних EPUB, але є набагато важливішою для більшості користувачів. Якщо вам потрібна програма, яка завжди створює коректні EPUB, calibre не для вас. Це означає, що якщо ви захочете надіслати створений за допомогою calibre файл EPUB до інтернет-сховища, де використано засіб перевірки коректності EPUB, вам доведеться забезпечити коректність коду EPUB власноруч. Calibre не зробить цього за вас. Іншими словами, вам слід передавати програмі лише коректні вхідні дані XHTML + CSS.

Як скористатися деякими додатковим можливостями інструментів перетворення?

Ви можете отримати довідку щодо окремих можливостей засобів перетворення у відповідь на наведення вказівника миші на елемент графічного інтерфейсу або на команду ebook-convert dummy.html .epub -h у терміналі. Чудовим місцем для початку є перегляд цього демонстраційного файла розширених можливостей: html-demo.zip [https://calibre-ebook.com/downloads/html-demo.zip].

Інтеграція з пристроями

Зміст

- Підтримку яких пристроїв передбачено у calibre?

- Як допомогти у реалізації підтримки вашого пристрою у calibre?

- Чому calibre не може виявити мій пристрій?

- Нестандартний або незвичайний пристрій. Як з’єднати з ним комп’ютер?

- Як скористатися calibre для роботи з iPad/iPhone/iPod touch?

- Як скористатися calibre на моєму телефоні або планшеті під керуванням Android чи Kindle Fire?

- Чи можна отримувати доступ до книг calibre за допомогою програми для перегляду інтернету на Kindle або іншому пристрої для читання?

- Чому у мене не виходити надсилати електронну пошту за допомогою calibre?

- Пристрій для читання книг монтується у Linux у режимі лише читання. Calibre не зможе отримувати до них доступу?

- Чому у calibre не передбачено підтримки збірок на Kindle або шаф на Nook?

- Коли я намагаюся скористатися calibre з моїм Kobo Touch, Glo тощо, програма показує повідомлення про помилку. Що робити?

- Зображення обкладинок книг, які ви надсилаєте на Kindle з’являються на секунду, а потім замінюються на типове зображення обкладинки.

- Kindle не показує обкладинки для файлів MOBI на PC/Kindle, на Android/iPad тощо

- Мною перенесено декілька книжок на Kindle за допомогою calibre. Чому вони не з’явилися у бібліотеці пристрою?

Підтримку яких пристроїв передбачено у calibre?

calibre може безпосередньо з’єднуватися з усіма поширеними (і більшістю малопоширених) пристроями для читання електронних книг, смартфонами, планшетами тощо. Крім того, за допомогою можливості З’єднатися з текою ви можете скористатися будь-яким пристроєм для читання електронних книг, який показує себе у системі як диск USB. Нарешті, ви можете встановити бездротове з’єднання із будь-яким пристроєм, де є програма для перегляду інтернету, за допомогою сервера даних Calibre.

Як допомогти у реалізації підтримки вашого пристрою у calibre?

Якщо ваш пристрій ідентифікується системою як диск USB, додавання його підтримки до calibre є дуже простим. Для цього нам знадобляться від вас такі дані:

- Повний список форматів електронних книг, підтримку яких передбачено на вашому пристрої.

- Чи є на пристрої спеціальна тека, де повинні зберігатись всі книжкові файли? Чи знайде пристрій файли, розміщені у підтеках?

- Нам також знадобляться дані щодо вашого пристрою, які calibre збере автоматично. По-перше, якщо на вашому пристрої передбачено підтримку карток SD, вставте їх. Далі, з’єднайте ваш пристрій з комп’ютером. У calibre скористайтеся пунктом Налаштування  →  Різне і натисніть кнопку «Діагностика виявлення пристроїв». У відповідь буде створено діагностичні дані. Скопіюйте дані до файла і повторіть процедуру, цього разу з від’єднаним від вашого комп’ютера пристроєм.

- Надішліть обидва набори виведених даних нам із іншою інформацією, і ми напишемо драйвер для вашого пристрою.

Після надсилання вами виведених даних для певної операційної системи підтримка пристрою у відповідній операційній системі з’явиться у наступному випуску calibre. Щоб надіслати нам дані, створіть сповіщення щодо вади і долучіть виведені дані до нього. Див. як звітувати щодо вад у calibre [https://calibre-ebook.com/uk/bugs].

Чому calibre не може виявити мій пристрій?

Виконайте ці кроки, щоб визначити причину:

- Переконайтеся, що з вашим комп’ютером одночасно з’єднано лише один пристрій. Не з’єднуйте з комп’ютером інших підтримуваних пристроїв, зокрема iPhone/iPad тощо.

- Якщо ви встановлюєте з’єднання із пристроєм виробництва Apple (iPad, iPod Touch, iPhone), слід зважати на те, що компанія Apple припинила підтримку можливості встановлення з’єднання за допомогою стороннього програмного забезпечення та кабеля USB. Замість цього, слід використовувати бездротове з’єднання за допомогою сервера даних calibre.

- Якщо ви з’єднуєтеся із Kindle Fire або іншим пристроєм під керуванням Android, ознайомтеся із приміткою щодо За допомогою кабеля USB.

- У macOS, якщо у вас виникають проблеми із правами доступу при з’єднанні пристрою із calibre, ви можете усунути їх за допомогою сторінки Налаштування системи > Безпека і конфіденційність > Конфіденційність > Файли і теки.

- Переконайтеся, що ви користуєтеся найсвіжішою версією calibre (зараз це 7.10.0).. Найсвіжішу версію завжди можна отримати з сайта calibre [https://calibre-ebook.com/uk/download]. Дізнатися про те, якою версією calibre ви користуєтеся, можна за допомогою даних у нижньому рядку головного вікна calibre.

- Впевніться, що ваша операційна система бачить пристрій. Тобто, пристрій відображається у Windows Explorer (на Windows) чи у Finder (на macOS).

- У calibre, відкрийте Налаштування  →  Ігноровані пристрої і перевірте, чи немає вашого пристрою у списку ігнорування.

- Якщо усі описані вище кроки не допомагають, перейдіть на сторінку Налаштування  →  Різне і натисніть кнопку Діагностика виявлення пристроїв, перед цим з’єднавши ваш комп’ютер із пристроєм. Далі, додайте виведені дані до квитка у системі стеження за вадами у calibre [https://bugs.launchpad.net/calibre].

Нестандартний або незвичайний пристрій. Як з’єднати з ним комп’ютер?

Окрім пункту З’єднатися з текою, доступ до якого можна отримати за допомогою кнопки З’єднатись/Роздати, у calibre передбачено додаток пристроїв Визначено користувачем, яким можна скористатися для встановлення з’єднання із будь-яким пристроєм USB, який може працювати у режимі диска у вашій операційній системі. Зауваження: у Windows, щоб програма calibre могла скористатися пристроєм, з ним має бути пов’язано літеру. Щоб дізнатися більше, див. додаток пристрою Налаштування -> Додатки -> Додатки «Інтерфейс пристрою» -> User Defined і Налаштування -> Різне -> Отримати дані для налаштовування пристрою, визначеного користувачем. Зауважте, що якщо ви використовуєте визначений користувачем додаток для пристрою, який зазвичай виявляється вбудованим додатком calibre, вам слід спочатку вимкнути вбудований додаток, щоб замість нього можна було використати визначений вами додаток.

Як скористатися calibre для роботи з iPad/iPhone/iPod touch?

Простим способом навігації вашою збіркою calibre з пристрою Apple є використання сервера даних calibre, який робить вашу збірку доступною з інтернету. Спочатку виконайте такі кроки у calibre:

- Встановіть бажаний формат виведення у calibre у значення EPUB (формат виведення можна вказати на сторінці Налаштування  →  Інтерфейс  →  Поведінка)

- Встановіть профіль виведення у iPad (це також працює для iPhone/iPod) на сторінці Налаштування  →  Перетворення  →  Загальні параметри  →  Налаштування сторінки

- Перетворіть книги, які ви читатимете на вашому i-пристрої на формат EPUB: просто позначте їх у списку і натисніть кнопку Перетворити.

- Увімкніть сервер даних натисканням кнопки З’єднатись/Роздати і залиште calibre працювати. Ви також можете наказати calibre автоматично запускати сервер даних за допомогою сторінки Налаштування  →  Спільний доступ  →  Обмін мережею.

Сервер даних надає вам змогу читати книги безпосередньо за допомогою Safari. Крім того, для пристроїв Apple створено багато програм, які можуть встановлювати з’єднання із сервером даних calibre. Ось приклади таких програм: Marvin, Mapleread та iBooks.

Користування сервером даних

Запустіть програму Safari і введіть IP-адресу і порт комп’ютера, на якому запущено сервер calibre. Ось так:

http://192.168.1.2:8080/

Замініть ``192.168.1.2``на локальну IP-адресу комп’ютера, на якому запущено calibre. Докладний опис запуску сервера та визначення належної IP-адреси наведено у розділі Сервер даних calibre.

У Safari ви побачите список книг. Після натискання пункту у ньому програма запропонує вам або отримати дані файла або прочитати файл у самій програмі для перегляду інтернету. Якщо ви виберете варіант із отриманням даних, Safari запропонує вам відкрити книгу у iBooks.

У багатьох засобах для читання передбачено підтримку навігації бібліотекою calibre безпосередньо за допомогою її інтерфейсу OPDS. У таких програмах ви можете перейти на сторінку інтернет-каталогу і додати IP-адресу сервера calibre для навігації і отримання книг з вашої бібліотеки calibre у самій програмі.

Як скористатися calibre на моєму телефоні або планшеті під керуванням Android чи Kindle Fire?

Існує два способи з’єднати ваш пристрій під керуванням Android із calibre. За допомогою кабелю USB або за допомогою бездротового зв’язку. Першим кроком у використанні пристрою під керуванням Android є встановлення на нього програми для читання електронних книг. Існує багато безкоштовних і комерційних програм для Android. Ось декілька прикладів (без певного пріоритету): 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].

За допомогою кабеля USB

Просто з’єднайте ваш пристрій з комп’ютером за допомогою кабелю USB. calibre має автоматично виявити пристрій. Потім ви можете перенести книги на пристрій за допомогою натискання кнопки Надіслати на пристрій. Зауважте, що у macOS і Linux із пристроєм на Android може одночасно працювати лише одна програма, тому переконайтеся, що пристрій не відкрито у програмі для керування файлами системи або у допоміжному засобів передавання файлів на Android тощо.

Примітка

На новіших пристроях під керуванням Android вам доведеться скористатися певними хитрощами, щоб змусити з’єднання працювати, оскільки Google намагається ускладнити роботу поза «хмарою» компанії. По-перше, розблокуйте екран, перш ніж вставляти кабель USB. Після вставляння кабелю USB ви побачите контекстне сповіщення. У ньому має бути повідомлення щодо «Передавання мультимедійних файлів» («Transferring Media files») або «MTP (Media Transfer mode)». Якщо кнопки вибору немає, натисніть сповіщення і змініть режим на режим передавання мультимедійних даних (MTP). Можливо, на цьому кроці вам доведеться перезапустити calibre, щоб ваш пристрій було розпізнано. Нарешті, під час кожної спробуи з’єднання calibre або операційної системи з пристроєм може з’явитися сповіщення щодо дозволу на з’єднання. Натисніть ньому кнопку «Гаразд» («OK»).

Примітка

При роботі із Kindle Fire 8 та новішими версіями цього пристрою піктограма, яку буде показано при вставлянні кабелю USB, вказуватиме на заряджання пристрою. Торкніться її і перемкніть пристрій у режим передавання даних, потім запустіть calibre — пристрій має бути виявлено.

За допомогою бездротового зв’язку

У calibre вбудовано вебсервер, Сервер даних calibre. Він робить доступною вашу збірку calibre у мережі. Ви можете здійснювати навігацію збіркою на вашому пристрої за допомогою простої програми для перегляду інтернету або спеціалізованої програми. Спочатку, слід виконати такі дії у calibre:

- Встановіть Бажаний формат виведення у calibre у значення EPUB для звичайних пристроїв під керуванням Android або MOBI для пристроїв Kindle (формат виведення можна вказати на сторінці Налаштування  →  Інтерфейс  →  Поведінка)

- Перетворіть книги, які ви читатимете на вашому i-пристрої на формат EPUB: просто позначте їх у списку і натисніть кнопку Перетворити.

- Увімкніть сервер даних у налаштуваннях calibre і не закривайте вікно calibre.

Далі, на вашому пристрої під керуванням Android відкрийте браузер і перейдіть до сторінки

http://192.168.1.2:8080/

Замініть ``192.168.1.2``на локальну IP-адресу комп’ютера, на якому запущено calibre. Докладний опис запуску сервера та визначення належної IP-адреси наведено у розділі Сервер даних calibre.

Тепер ви можете переглядати вміст вашої збірки книг та отримувати книги з calibre на ваш пристрій, а отже відкривати книги за допомогою будь-якої програми для читання електронних книг, яку встановлено на вашому пристрої під керуванням Android.

У багатьох засобах для читання передбачено підтримку навігації бібліотекою calibre безпосередньо за допомогою її інтерфейсу OPDS. У таких програмах ви можете перейти на сторінку інтернет-каталогу і додати IP-адресу сервера calibre для навігації і отримання книг з вашої бібліотеки calibre у самій програмі.

Чи можна отримувати доступ до книг calibre за допомогою програми для перегляду інтернету на Kindle або іншому пристрої для читання?

У calibre передбачено сервер даних, який експортує список книг у calibre у форматі вебсторінки. Докладніше про це у розділі Сервер даних calibre.

На деяких пристроях, зокрема Kindle (1/2/DX), доступ до порту 8080 (типового порту, на якому працює сервер) заборонено. Якщо на вашому пристрої доступ до цього порту заборонено, змініть порт у налаштуваннях calibre на 80. (У деяких операційних системах не можна запускати сервер на порту із номером, який є меншим за 1024, з міркувань безпеки. У такому випадку найпростішим рішенням є налаштовування вашого маршрутизатора на переспрямоування запитів до порту 80 на порт 8080).

Крім того, програми для перегляду інтернету на деяких пристроях недостатньо розвинені, щоб показувати програмоподібний інтерфейс сервера даних. На таких пристроях ви можете просто додати /mobile до адреси сервера, щоб отримати доступ до спрощеного інтерфейсу без JavaScript.

Чому у мене не виходити надсилати електронну пошту за допомогою calibre?

Через значну частку спаму у електронній пошті, надсилання повідомлень електронної пошти може бути проблемним, оскільки на різних поштових серверах використовують різні стратегії для блокування спаму. Найпоширенішою проблемою є безпосереднє надсилання пошти (без ретрансляції) у calibre. Багато серверів (наприклад, сервери Amazon) блокують поштові повідомлення, які не надходять від добре відомого ретранслятора. Найстійкішим до подібних проблем способом налаштувати надсилання електронної пошти у calibre є такий:

- Створіть безкоштовний обліковий запис GMX на GMX [https://www.gmx.com].

- Відкрийте сторінку Налаштування  →  Спільний доступ  →  Обмін електронною поштою у calibre, натисніть кнопку Використовувати GMX і вкажіть потрібні програмі значення.

- Увійдіть до вашого облікового запису GMX на сайті і увімкніть надсилання за допомогою SMTP (Settings->POP3 & IMAP->Send and receive emails via external program)

- Після цього calibre зможе використовувати GMX для надсилання поштових повідомлень.

- Якщо ви надсилатимете дані на ваш пристрій Kindle, не забудьте оновити параметри електронної пошти на вашій сторінці Amazon Kindle, щоб дозволити надсилання електронної пошти з вашої адреси на GMX. Крім того, зауважте, що Amazon забороняє отримання електронною поштою книг у форматі AZW3 та книг у новому стилі (KF8) MOBI. Нарешті, нещодавно Amazon почала розсилати повідомлення електронної пошти із проханням натиснути посилання на ваш обліковий запис GMX, перш ніж книгу буде надіслано.

Навіть після виконання цих дій у вас все ж можуть виникнути проблеми. Типовим джерелом проблем є те, що деякі не дуже вдало побудовані антивірусні програми забороняють calibre відкривати з’єднання для надсилання електронної пошти. Спробуйте створити запис виключення для calibre у вашій антивірусній програмі.

Примітка

Microsoft або GMX можуть вимкнути ваш обліковий запис, якщо ви регулярно надсилатимете значний обсяг електронної пошти. Отже, якщо ви користуєтеся цими службами для надсилання пошти, calibre автоматично обмежуватиме надсилання книг однією книгою кожні п’ять хвилин. Якщо ви вважаєте, що ризику блокування облікового запису немає, ви можете зменшити цей інтервал очікування за допомогою сторінки налаштувань Налаштування  →  Додатково  →  Коригування у calibre.

Примітка

Нещодавно Google довільним чином порушив стандарти підтримки протоколу надсилання електронної пошти (SMTP), намагаючись змусити усіх користуватися інтернет-інтерфейсом, де може бути показано більше реклами Компанія стверджує, що протокол SMTP є незахищеним. Це твердження є помилковим і не має ніякого підґрунтя. Скористайтеся якимось іншим надавачем послуг електронної пошти.

Примітка

Якщо ви не хочете надавати calibre доступ до вашого робочого облікового запису електронної пошти, створіть інший обліковий запис на службі надання безкоштовних послуг доступу до електронної пошти, наприклад GMX або Outlook, і користуйтеся ним у calibre.

Пристрій для читання книг монтується у Linux у режимі лише читання. Calibre не зможе отримувати до них доступу?

Ядра Linux монтують пристрої у режимі лише читання, якщо відповідні файлові системи містять помилки. Відновити працездатність файлової системи можна так:

sudo fsck.vfat -y /dev/sdc

Замініть /dev/sdc на шлях до вузла пристрою. Визначити вузол вашого пристрою, який завжди розташовуватиметься у /dev, можна за допомогою даних, які буде виведено цією командою:

mount

Чому у calibre не передбачено підтримки збірок на Kindle або шаф на Nook?

Ні у Kindle, ні у Nook не передбачено жодного способу керування збірками за допомогою з’єднання USB. Якщо ви справді хочете користуватися збірками, рекомендуємо вам продати ваш Kindle/Nook і придбати Kobo. Лише розробники Kobo, здається, розуміють, що життя є надто коротким для введення назв збірок одна за одною за допомогою екрана з електронним чорнилом. 😇

Зауважте, що у випадку з Kindle існує один спосіб керування збірками за допомогою USB, але він потребує перезавантаження Kindle кожного разу, коли ви від’єднуватимете пристрій від комп’ютера, щоб програмне забезпечення пристрою змогло розпізнати зміни у збірках. Отже, малоймовірно, що у когось із розробників calibre колись виникне достатня мотивація для реалізації підтримки цього способу. Втім, існує додаток до calibre, який надає вам змогу створювати збірки на вашому пристрої Kindle на основі метаданих calibre. Його можна отримати тут [https://www.mobileread.com/forums/showthread.php?t=244202].

Примітка

Компанія Amazon повністю усунула можливість керування збірками у нових моделях своїх пристроїв, зокрема у Kindle Touch і Kindle Fire. Після цього навіть вказаний вище додаток втрачає сенс, якщо ви не отримаєте права адміністратора на вашому Kindle і не встановите нетипову мікропрограму.

Коли я намагаюся скористатися calibre з моїм Kobo Touch, Glo тощо, програма показує повідомлення про помилку. Що робити?

Мікропрограма Kobo сповнена вад. Не можна навіть вгадати, коли не вдасться встановити з’єднання із пристроєм. Причиною неможливості встановлення з’єднання може бути певна комбінація материнської плати, портів, кабелів, концентраторів USB. Якщо бачите повідомлення про помилку, коли намагаєтеся з’єднати ваш пристрій із calibre, спробуйте виконати вказані нижче дії, які допомогли деяким з користувачів calibre.

- З’єднайте Kobo безпосередньо з вашим комп’ютером, без концентратора USB.

- Спробуйте інший кабель USB або інший порт USB на вашому комп’ютері.

- Вийдіть з Kobo і увійдіть знову. Таким чином ви накажете системі перезібрати базу даних і виправити помилки, які пов’язано із пошкодженнями у базі даних.

- Спробуйте оновити мікропрограму (прошивку) на вашому Kobo Touch до найсвіжішої версії.

- Спробуйте скинути налаштування Kobo до типових (іноді це на певний час вирішує проблему, але потім вона з’являється знову, тому вам доведеться скидати налаштування знову і знову).

- Спробуйте надсилати на Kobo одну чи дві книги за раз і не зберігайте на Kobo великих збірок.

Зображення обкладинок книг, які ви надсилаєте на Kindle з’являються на секунду, а потім замінюються на типове зображення обкладинки.

Причиною є вада у Amazon. Вони намагаються отримати зображення обкладинки з власних серверів. Коли спроба зазнає невдачі, вони замінюють наявне зображення обкладинки calibre зображенням на основі типової обкладинки. Подробиці наведено у цій гілці обговорення на форумі [https://www.mobileread.com/forums/showthread.php?t=329945]. Починаючи з версії 4.17, у calibre реалізовано обхідний маневр — після з’єднання Kindle з calibre і знищення обкладинок Amazon calibre відновить зображення автоматично. Тому, для того, щоб бачити зображення обкладинок на вашому Kindle, вам слід виконати такі дії:

1. Надішліть книгу на Kindle за допомогою calibre

2. Від’єднайте Kindle і зачекайте, доки Amazon знищить обкладинку

3. Знову з’єднайте Kindle з calibre

Зауважте, що цей обхідний маневр працює лише для книг, які надіслано за допомогою calibre 4.17 або пізніше. Крім того, можна просто працювати з Kindle у режимі польоту — насправді, вам зовсім не потрібно, щоб Amazon знала про всі книги, які ви читаєте. Радимо вам звернутися до служби підтримки Amazon і поскаржитися щодо цієї вади. Можливо, Amazon прислухається до вас.

Примітка

Якщо ці поради не спрацюють, переконайтеся, що встановлено мікропрограму Kindle принаймні версії 5.12.5, яку випущено у квітні 2020 року.

Kindle не показує обкладинки для файлів MOBI на PC/Kindle, на Android/iPad тощо

Причиною є вада у програмному забезпеченні Amazon. Ви можете обійти її за допомогою сторінки налаштувань calibre Налаштування  →  Перетворення  →  Вихідні параметри  →  Вивід у MOBI і пункту «Увімкнути оприлюднення книги…». Якщо ви перетворюєте раніше перетворену книгу, вам також слід позначити цей пункт у діалоговому вікні перетворення для окремої книги (оскільки параметри перетворення окремих книг зберігаються і мають вищий пріоритет за загальні параметри).

Зауважте, що після цього створений файл MOBI буде показано у розділі особистих документів, а не розділі «Книги» на Kindle Fire, а Amazon whispersync не працюватиме. Втім, обкладинки буде показано. Отже, ви можете вибрати ту функціональну можливість, яка є для вас важливішою. Вам варто зв’язатися із Amazon і попросити їх виправити цю ваду.

Вада у програмному забезпеченні Amazon полягає у тому, що коли ви записуєте файл MOBI на Kindle, і файл не позначено як особистий документ, Amazon вважає, що ви придбали книгу у крамниці компанії і намагається отримати мініатюру обкладинки книги з серверів компанії. Якщо отримати мініатюру не вдасться, програма відмовляється використовувати зображення обкладинки з файла MOBI. Ймовірно, так зроблено навмисне з боку Amazon, щоб змусити авторів продавати книги лише за допомогою крамниці компанії. Іншими словами, Kindle показує лише обкладинки книг, які позначено як особисті документи, або обкладинки книг, які придбано у Amazon.

Якщо ви надішлете файл MOBI на пристрій Kindle з електронним чорнилом за допомогою calibre і з’єднання USB, calibre обійде цю ваду у програмному забезпеченні Amazon, вивантаживши зображення обкладинки програмно. Втім, такий обхідний маневр можливий, лише якщо використовується з’єднання USB і надсилання даних відбувається за допомогою calibre. Зауважте, що якщо файл буде надіслано електронною поштою, програмне забезпечення Amazon автоматично позначить файл MOBI як особистий документ, обкладинку буде показано, але пункт книги з’явиться лише у списку особистих документів.

Мною перенесено декілька книжок на Kindle за допомогою calibre. Чому вони не з’явилися у бібліотеці пристрою?

Книги, які надіслано на Kindle, буде показано лише після того, як їх буде проіндексовано Kindle. Індексація є доволі тривалим процесом. Якщо навіть доволі довге очікування не винагороджується появою книги у списку, ймовірною причиною є аварійне завершення засобу індексування книг Kindle. Іноді індексування певних книг цим засобом призводить до його аварії. На жаль, розробники Amazon не передбачили жодного способу визначити, обробка якої з книг призвела до аварії на Kindle. Єдиним способом усування проблеми є скидання Kindle до початкового стану або вилучення усіх файлів з пам’яті пристрою за допомогою Провідника Windows (або будь-якої іншої програми для керування файлами) із наступним надсиланням книг на пристрій, одна за одною, аж доки не буде виявлено проблемну книгу. Щойно ви виявите таку книгу, вилучіть її з Kindle і виконайте перетворення цієї книги з MOBI на MOBI або з MOBI на AZW3 у calibre, а потім знову надішліть її на пристрій. Найімовірніше, таким чином проблему буде усунено.

Керування бібліотекою

Зміст

- Де зберігаються файли книг?

- Яким чином calibre обробляє і впорядковує імена авторів?

- Чому calibre не дає зберігати книги із визначеною користувачем структурою тек?

- Чому у calibre немає стовпчика для мого типу даних?

- Чи можна так зробити, щоб з’явився стовпчик із даними щодо форматів або ISBN?

- Як перенести дані calibre з одного комп’ютера на інший?

- Список книг у calibre порожній!

- Чому програма показує повідомлення про помилку, якщо бібліотека calibre зберігається на диску у мережі або у сховищі NAS?

Де зберігаються файли книг?

Під час першого запуску calibre програма запитає вас про те, де слід зберігати ваші книги. Коли ви додаєте книгу до бібліотеки calibre, книгу буде скопійовано до цієї теки. Книги у теці розподілено за підтеками, назви яких визначаються авторами та назвами книг. Зауважте, що вмістом цієї теки в автоматичному режимі керує calibre. Не намагайтеся додавати файли або теки до цієї теки вручну, оскільки їх буде автоматично вилучено. Якщо ви хочете додати файл, пов’язаний із якоюсь з книг, скористайтеся для цього верхньою правою частиною діалогового вікна Редагувати метадані. Далі, calibre автоматично розмістить файл у належній теці і пересуватиме його відповідно, якщо ви внесене зміни до даних щодо авторів та назви книги.

Метадані щодо книг зберігаються у файлі metadata.db на верхньому рівні теки бібліотеки. Цей файл є базою даних sqlite. Під час створення резервних копій вашої бібліотеки вам слід скопіювати усю теку і усі її підтеки.

Тека бібліотеки і увесь її вміст складають бібліотеку calibre. У вас може бути декілька таких бібліотек. Щоб керувати бібліотеками, натисніть піктограму calibre на панелі інструментів. Ви можете створювати бібліотеки, вилучати або перейменовувати наявні бібліотеки, а також перемикатися між бібліотеками.

Ви можете копіювати або пересувати книги між різними бібліотеками (якщо вами налаштовано декілька бібліотек): просто наведіть вказівник миші на пункт книги у бібліотеці, клацніть правою кнопкою миші і виберіть у контекстному меню пункт Копіювати до бібліотеки.

Яким чином calibre обробляє і впорядковує імена авторів?

Імена авторів є складними записами, особливо через культурні відмінності у їхньому записі, див. цю нотатку [https://www.w3.org/International/questions/qa-personal-names.en.php?changelang=en], щоб дізнатися про деякі проблемні місця. У calibre передбачено дуже гнучку стратегію керування іменами авторів. По-перше, слід розуміти, що записи книг і авторів є окремими об’єктами у calibre. Книга може мати декілька авторів, а автор може бути автором декількох книг. Керувати записами авторів книги можна за допомогою діалогового вікна редагування метаданих. Змінити запис окремого автора можна клацанням правою кнопкою на пункті списку авторів панелі навігації мітками, розташованої ліворуч у головному вікні calibre. Далі, слід вибрати у контекстному меню пункт Керування записом «Автори». За допомогою відповідного діалогового вікна ви зможете змінити ім’я автора та спосіб упорядковування імені. У результаті буде автоматично змінено записи автора в усіх книгах цього автора. Якщо у книги декілька авторі, записи їхніх імен слід відокремлювати символом &.

Тепер, щодо упорядковування імен авторів:

- Коли до записів авторів calibre додається новий (таке трапляється кожного разу, коли додається книга нового автора), calibre автоматично обчислює рядок упорядковування як для книги, так і для автора.

- Записи авторів на панелі перегляду міток буде упорядковано за значенням упорядковування для авторів. Зауважте, що цей порядок відрізняється від поля упорядковування авторів для книги.

- Типово, цей алгоритм упорядковування припускає, що ім’я автора записано у формі Ім’я Прізвище, і створює значення упорядковування Прізвище, Ім’я.

- Змінити алгоритм можна за допомогою сторінки Налаштування  →  Додатково  →  Коригування, а саме, коригування author_sort_copy_method.

- Ви можете наказати calibre перевизначити значення упорядковування імен авторів для усіх авторів клацанням правою кнопкою миші на пункті запису будь-якого з авторів із наступним вибором у контекстному меню пункту Керування записом «Автори». Далі, слід натиснути кнопку Повторно обчислити всі значення впорядкування авторів. Виконайте цю дію після встановлення для коригування author_sort_copy_method бажаного для вас значення.

- Ви можете наказати calibre повторно обчислити значення упорядковування авторів для усіх книг за допомогою діалогового вікна пакетного редагування метаданих (позначте усі книги і натисніть кнопку редагування метаданих, позначте пункт Автоматично встановлювати впорядкування авторів і натисніть кнопку «Гаразд»).

- Під час перевизначення значень упорядковування імен авторів для книг calibre використовує значення упорядковування для кожного окремого автора. Тому, перш ніж перевизначати значення упорядковування імен авторів для книг, переконайтеся, що ці значення є правильними.

- Ви можете керувати тим, буде показано записи авторів на панелі міток за іменами чи за значенням упорядковування, за допомогою встановлення коригування categories_use_field_for_author_name на сторінці Налаштування  →  Додатково  →  Коригування.

Зауважте, що ви можете встановити будь-яке бажане значення упорядковування імен авторів за допомогою пункту контекстного меню Керування записом «Автори». Це корисно, якщо маєте справу з іменами, які calibre не може обробити правильно у автоматичному режимі, зокрема складними іменами, що складаються з багатьох частин, наприклад, Мігель де Сервантес Сааведра, або іменами азійських авторів, зокрема Сунь Цзи.

Через цю гнучкість програми ви можете налаштувати calibre так, щоб програма керувала іменами авторів саме так, як ви цього хочете. Наприклад, типовим запитом є налаштовування calibre на показ імен авторів у форматі «Прізвище, Ім’я». Для цього, якщо наведене нижче зауваження не стосується вашого випадку, виконайте такі дії:

- Встановіть для коригування author_sort_copy_method значення copy у описаний вище спосіб.

- Перезапустіть calibre. Не змінюйте метаданих книг перед виконанням решти кроків.

- Змініть усі імена авторів на формат «прізвище, ім’я» за допомогою діалогового вікна керування записами авторів.

- Після зміни записів усіх авторів натисніть кнопку «Повторно обчислити всі значення впорядкування авторів».

- Натисніть кнопку «Гаразд», і calibre внесе зміни у всі записи авторів усіх ваших книг. Виконання цієї дії може бути досить тривалим.

Примітка

Під час зміни з формату «Ім’я Прізвище» на формат «Прізвище, Ім’я» часто виявляється, що значення в author_sort вже записано у форматі «Прізвище, Ім’я». Якщо це так, виконайте такі дії:

- Встановіть для коригування author_sort_copy_method значення copy у описаний вище спосіб.

- Перезапустіть calibre. Не змінюйте метаданих книг перед виконанням решти кроків.

- Відкрийте діалогове вікно керування записами авторів. Натисніть кнопку Скопіювати всі значення впорядкування авторів до поля авторів.

- Перевірте, чи усе як слід. Ви можете натиснути кнопку «Скасувати», щоб відкинути зміни. Після натискання кнопки «Гаразд» внесені зміни вже не можна буде скасувати.

- Натисніть кнопку «Гаразд», і calibre внесе зміни у всі записи авторів усіх ваших книг. Виконання цієї дії може бути досить тривалим.

Чому calibre не дає зберігати книги із визначеною користувачем структурою тек?

Саме призначення усіх можливостей з керування бібліотекою у calibre полягає у тому, що ці можливості із інтерфейсом пошуку і упорядковування надають змогу знаходити книги набагато ефективніше за будь-яку можливу схему тек, якою б ви могли користуватися у вашій збірці. Справді, щойно ви почнете почуватися комфортно з інтерфейсом calibre для пошуку, упорядковування та навігації вашою збіркою, у вас уже не виникатиме потреби у полюванні за файлами на диску з метою знайти потрібну вам книгу. Розподіляючи книги за власною структурою тек у форматі Автори -> Назва -> Файли книги, програма calibre здатна досягти високого рівня надійності і стандартизації. Щоб проілюструвати те, чому інтерфейс на основі пошуку і міток має переваги над простим розподілом книг за теками, розгляньмо приклад. Нехай збірку книг розподілено за теками із такою схемою:

Genre -> Author -> Series -> ReadStatus

За такої схеми дуже просто знайти, наприклад, усі книги із науковою фантастикою Айзека Азімова з циклу «Фундація». Але припустімо тепер, що ви хочете знайти усі непрочитані книги, які належать до жанру наукової фантастики. Немає простого способу зробити це за допомогою розподілу книг за теками. Вам доведеться змінити схему тек на таку:

ReadStatus -> Genre -> Author -> Series

У calibre же вам достатньо скористатися мітками для позначення жанру і стану прочитаності, а потім просто виконати пошук tag:scifi and not tag:read. У calibre навіть є чудовий графічний інтерфейс, отже вам не доведеться вчити мову запитів для пошуку, — достатньо просто клацнути на пунктах міток, щоб включити або виключити мітки з пошуку.

Для тих, кому потрібен доступ до файлової системи, щоб мати доступ до книг мережею, у calibre передбачено чудовий сервер даних, який надасть вам доступ до вашої бібліотеки calibre мережею.

Якщо ви побоюєтеся того, що одного дня розробку calibre буде припинено і усі ваші книги лишаться у непотрібній вже структурі каталогів програми, скористайтеся потужною можливістю Зберегти на диск calibre. Ви зможете експортувати ваші файли до структури каталогів довільної складності на основі метаданих книг.

Нарешті, числа наприкінці кожної теки з назвою книги призначено для забезпечення стійкості системи. Число — це номер-ідентифікатор запису книги у базі даних calibre. Наявність числа надає змогу мати декілька записів із однаковими даними щодо назви книги і імен авторів. Це також частина системи, яка уможливлює повторне створення бази даних із усіма метаданими, якщо раптом файл бази даних буде пошкоджено. Беручи до уваги те, що призначенням calibre є позбутися зберігання метаданих у назвах файлів та використання файлової системи для пошуку книг, збільшення стійкості системи додаванням чисел-ідентифікаторів до назв тек є вартим незначних проблем із некрасивими назвами тек.

Якщо вас все ж не вдалося переконати цими аргументами, боюся, що calibre не для вас. Пошукайте якусь іншу програму для каталогізації вашої збірки. Щоб ви розуміли усе точніше, — усі ці принципи, на яких побудовано програму, не буде змінено. Будь ласка, не намагайтеся змінити думку розробників марними повідомленнями електронною поштою чи дописами на форумах.

Чому у calibre немає стовпчика для мого типу даних?

У calibre передбачено типові стовпчики для найпоширеніших полів даних. Крім того, ви, звичайно ж, можете додати будь-які потрібні вам стовпчики. Для додавання стовпчиків скористайтеся пунктом Налаштування  →  Інтерфейс  →  Додати нетиповий стовпчик. Ознайомтеся із розділом підручника щодо графічного інтерфейсу [https://calibre-ebook.com/uk/demo#tutorials], щоб дізнатися про те, як створювати власні стовпчики, або прочитайте цей допис у блозі [https://blog.calibre-ebook.com/calibre-custom-columns/].

Ви також можете створювати «віртуальні стовпчики», які є поєднанням метаданих з інших стовпчиків. У діалоговому вікні додавання стовпчика скористайтеся посиланнями Швидко створити для створення стовпчиків, де буде показано ISBN або формати книги. Ви можете скористатися потужною мовою шаблонів calibre для виконання додаткових дій із стовпчиками. Докладніший опис можна знайти у розділі Мова шаблонів calibre.

Чи можна так зробити, щоб з’явився стовпчик із даними щодо форматів або ISBN?

Так, можна. Виконайте настанови із наведеної вище відповіді щодо додавання нетипових стовпчиків.

Як перенести дані calibre з одного комп’ютера на інший?

Ви можете експортувати усі дані calibre (книги, параметри і додатки), а потім імпортувати ці дані на іншому комп’ютері. Спочатку розглянемо процедуру імпортування даних:

- Клацніть на кнопці зі стрілочкою праворуч від кнопки із піктограмою calibre на головній панелі інструментів calibre і виберіть пункт Експортувати або імпортувати усі дані calibre. Зауважте, що якщо з комп’ютером буде з’єднано пристрій, цей пункт меню стане недоступним. Тому спочатку слід від’єднати усі з’єднані пристрої. Далі, натисніть кнопку із міткою Експортувати усі ваші дані calibre. У відповідь ви побачите список усіх ваших бібліотек calibre. Натисніть кнопку Гаразд і виберіть порожню теку десь на диску вашого комп’ютера. До цієї теки буде експортовано усі дані. Далі, просто скопіюйте цю теку на новий комп’ютер і виконайте наведені нижче настанови щодо імпортування даних.

- Встановіть calibre на ваш новий комп’ютер і пройдіть усіма кроками майстра першого запуску. Немає значення, які параметри ви виберете, оскільки на наступному кроці ви імпортуєте усі ваші збережені параметри роботи програми. Після завершення роботи майстра у вас буде порожня бібліотека calibre, у якій міститиметься лише книга Calibre Quick Start Guide. Знову натисніть кнопку зі стрілочкою, розташовану праворуч від кнопки з піктограмою calibre, і виберіть у контекстному меню пункт Експортування або імпортування усіх даних calibre. Далі, натисніть кнопку з міткою Імпортувати раніше експортовані дані. Виберіть теку, до якої ви раніше скопіювали експортовані дані. У відповідь програма покаже список бібліотек, які ви можете імпортувати. Пройдіться списком і вкажіть нове місце для кожної з бібліотек (місцем може бути будь-яка порожня тека на вашому новому комп’ютері). Натисніть кнопку Гаразд. Щойно імпортування даних буде завершено, calibre перезапуститься, і ви зможете скористатися усіма вашими старими бібліотеками, параметрами і додатками calibre.

Примітка

Ці функціональні можливості імпортування та експортування даних доступні лише у версіях calibre, починаючи з 2.47. Якщо ви користуєтеся старішою версією calibre або маєте проблеми із імпортуванням та експортуванням даних, ви можете просто скопіювати усю теку вашої бібліотеки calibre вручну, як це описано у наступному абзаці.

Просто скопіюйте теку бібліотеки calibre зі старого комп’ютера на новий. Ви можете визначити місце, де зберігається тека бібліотеки, натисканням кнопки з піктограмою calibre на панелі інструментів вікна програми. Першим пунктом буде показано шлях до теки бібліотеки.

Тепер, на новому комп’ютері запустіть calibre вперше. Програма покаже вікно майстра початкового налаштовування, де запитає вас про розташування бібліотеки calibre. Вкажіть раніше скопійовану теку. Якщо на комп’ютері, куди ви переносите дані, вже було встановлено calibre, майстер початкового налаштовування буде недоступним. У цьому випадку, натисніть кнопку зі стрілочкою, розташовану праворуч від кнопки з піктограмою calibre на панелі інструментів головного вікна програми і вкажіть скопійовану теку у полі теки бібліотеки. Тепер на вашому комп’ютері буде дві бібліотеки calibre, і ви зможете перемикатися між ними натисканням кнопки з піктограмою calibre на панелі інструментів. Перенесення бібліотеки у цей спосіб зберігає усі метадані, мітки, нетипові стовпчики тощо.

Список книг у calibre порожній!

Щоб зрозуміти, чому таке трапляється, вам слід зрозуміти, що таке бібліотека calibre. На найбільш базовому рівні бібліотека calibre — це просто тека. Кожного разу, коли ви додаєте книгу до бібліотеки calibre, файли книги копіюються до цієї теки (з упорядковуванням за підтеками на основі даних про автора і назву книги). У теці бібліотеки calibre, на верхньому рівні, ви побачите файл із назвою metadata.db. Це файл, у якому calibre зберігає метадані, зокрема дані щодо назв, авторів, оцінок, міток кожної з книг у вашій бібліотеці calibre. Список книг, який показує calibre, створюється на основі даних, прочитаних з файла metadata.db.

Існує дві причини, чому calibre показує порожній список книг:

- Місце зберігання вашої бібліотеки calibre було змінено. Таке могло трапитися, якщо вона зберігалася на зовнішньому диску, і літера диска змінилася. Ви також могли пересунути цю теку. У цьому випадку calibre не може знайти свою бібліотеку і розпочинає роботу із порожньої бібліотеки. Щоб повернути дані, натисніть кнопку зі стрілочкою, розташовану праворуч від кнопки з піктограмою calibre на панелі інструментів головного вікна calibre і виберіть у контекстному меню пункт Змінити або створити бібліотеку. Натисніть невеличку синю піктограму, щоб вибрати нове місце зберігання вашої бібліотеки calibre, і натисніть кнопку Гаразд. Якщо ви не пам’ятаєте нового місця зберігання бібліотеки, пошукайте на комп’ютері файл metadata.db.

- Ваш файл metadata.db було вилучено або пошкоджено. У цьому випадку ви можете наказати calibre повторно побудувати metadata.db на основі резервних копій. Для цього наведіть вказівник миші на піктограму calibre на панелі інструментів вікна calibre, натисніть кнопку, розташовану праворуч від основної кнопки і виберіть пункт Обслуговування бібліотеки  →  Відновити базу даних. Після цього calibre автоматично повторно побудує metadata.db.

Чому програма показує повідомлення про помилку, якщо бібліотека calibre зберігається на диску у мережі або у сховищі NAS?

Не зберігайте вашу бібліотеку calibre на диску у мережі.

Файлова система — складна штука. У більшості мережевих файлових систем не вистачає різноманітних можливостей, які використовує calibre. У деяких системах не передбачено підтримки блокування файлів, у деяких — створення жорстких посилань, деякі просто ненадійні. Крім того, calibre — програма, яка коректно працює, лише коли запущено її єдиний екземпляр. Якщо ви запустите декілька екземплярів calibre із використанням однієї бібліотеки для роботи з мережею, можливі різноманітні помилки у роботі. Нарешті, у різних операційних системах накладаються різні обмеження на файлові системи. Отже, якщо ви використовуватимете сховище даних у мережі для роботи із різними операційними системами, помилки у роботі трапляться напевне.

Спробуйте скористатися сервером даних calibre, щоб зробити ваші книги доступними з інших комп’ютерів. Запустіть calibre на окремому комп’ютері і отримуйте доступу до нього за допомогою сервера даних або можливостей віддаленої стільниці.

Якщо вам справді потрібно використовувати вашу бібліотеку з декількох комп’ютерів, скористайтеся інструментом синхронізації даних, зокрема DropBox або rsync, замість використання диска у мережі. Якщо ви користуватиметесь інструментами синхронізації вмісту файлів, критично важливо забезпечити таку роботу calibre і засобів синхронізації вмісту файлів, щоб уникнути одночасного доступу цих інструментів до бібліотеки calibre. Іншими словами, уникайте одночасної роботи засобів синхронізації вмісту файлів і calibre.

Втім, навіть використання засобів синхронізації не убезпечить вас від можливого пошкодження даних або їхньої втрати. Отже, використовуйте мережеві сховища даних, якщо втрата даних не є для вас критичною. Зокрема, вам слід знати, що Диск Google є несумісним з calibre. Якщо ви розташуєте вашу бібліотеку calibre на Диску Google, ви втратите дані. Див. цю гілку обговорення [https://www.mobileread.com/forums/showthread.php?t=205581], щоб дізнатися більше.

Різне

Зміст

- Amazon припиняє надсилання файлів MOBI електронною поштою?

- Як зробити так, щоб calibre отримувала новини з вашого улюбленого сайта новин?

- Чому програму названо calibre?

- Чому calibre показує лише частину встановлених шрифтів у macOS?

- Чому calibre не запускається у Windows?

- Чому calibre часом повисає або аварійно завершує роботу?

- Чому засіб для перегляду електронних книг calibre та засоби редагування книг не працюють у Windows?

- Використання засобу перегляду або виконання перетворень призводить до повідомлення про заборону доступу (permission denied) у Windows. Що робити?

- Чому calibre не запускається або аварійно завершує роботу у macOS?

- Після запуску засобу перегляду книг calibre видно лише біле або чорне полотно. Що робити?

- Мною отримано програму для встановлення. Чому вона не спрацьовує?

- Антивірусна програма стверджує, що calibre є вірусом або троянською програмою. Щоб робити?

- Як створити резервну копію даних calibre?

- Як прочитати придбані книги EPUB у calibre (або що робити з файлами .acsm)?

- Бачу повідомлення «Доступ заборонено» («Permission Denied»). Що робити?

- Як зробити, щоб метадані коментаря було показано на пристрої для читання?

- Як налаштувати calibre на використання проксі-сервера HTTP?

- Я хочу реалізувати якісь нові можливості у calibre. Як це зробити?

- Чому у calibre немає системи автоматичного оновлення?

- Якими є умови ліцензування calibre?

- Як запустити calibre з флешки?

- Як скористатися частинами calibre, зокрема засобом отримання новин та сервером даних, на сервері під керуванням Linux?

Amazon припиняє надсилання файлів MOBI електронною поштою?

Компанія Amazon оголосила [https://blog.the-ebook-reader.com/2022/05/03/amazon-dropping-mobi-support-on-send-to-kindle-apps/], що припиняє приймати файл MOBI, які надіслано електронною поштою на адреси @kindle.com. Ви можете наказати calibre надсилати EPUB замість MOBI за допомогою сторінки Налаштування → Обмін ел. поштою, вилучивши MOBI зі списку форматів надсилання на вашу адресу електронної пошти @kindle.com і додавши туди EPUB.

Втім, слід зауважити, що обробка Amazon файлів EPUB не завжди є коректною: може бути відмовлено в обробці файлів EPUB, які працюють деінде. У таких випадках ви можете скористатися чимось подібним до такої схеми:

1. Перетворіть файл EPUB на MOBI

2. Потім перетворіть файл MOBI знову на EPUB і надішліть отриманий файл EPUB

У результаті перетворення буде усунуто усе розширене форматування, вбудовані шрифти тощо, але значно збільшено шанси на вдале прийняття Amazon файла EPUB.

Примітка

Якщо ви раніше користувалися доставленням електронною поштою періодичних видань, дані яких отримувала calibre, вам варто скористатися передаванням цих даних кабелем USB або отриманням їх з сервера даних calibre за допомогою вбудовано до Kindle браузера. Втім, якщо ви захочете продовжити користуватися доставленням електронною поштою, ви можете спробувати змінити формат виведення на сторінці «Налаштування->Поведінка» на EPUB, і calibre отримуватиме новини у форматі EPUB. Чи буде прийнятним для Amazon отриманий EPUB є цілком іншим питанням.

Як зробити так, щоб calibre отримувала новини з вашого улюбленого сайта новин?

Якщо ви достатньо знаєтеся на роботі з комп’ютерами ви можете навчити calibre отримувати новини з будь-якого сайта. Докладніше про це у розділі Додавання вашого улюбленого сайта новин.

Крім того, ви можете попросити реалізувати для вас отримання новин із сайта за допомогою форуму рецептів calibre [https://www.mobileread.com/forums/forumdisplay.php?f=228].

Чому програму названо calibre?

Виберіть будь-яку з причин:

- Converter And LIBRary for Ebooks (перетворювач і бібліотека для електронних книг)

- Високо*каліберний* продукт

- Присвята SONY Librie, першому пристрою для читання електронних книг на основі технології електронного чорнила.

- Цю назву вибрала жінка автора ;-)

calibre слід вимовляти як «ка-лі-бер», а не «ка-лі-бре». Якщо точніше, calibre — британський запис слова caliber, який використовується у країнах Співдружності. Оскільки автор програми є індійцем, для нього такий спосіб запису слова є природнім.

Чому calibre показує лише частину встановлених шрифтів у macOS?

Calibre вбудовує шрифти до файлів електронних книг, які створює програма. У файлах електронних книг передбачено підтримку вбудовування лише шрифтів OpenType (.ttf та .otf). Більшість шрифтів у системах macOS зберігаються у форматі .dfont, отже їх не можна вбудовувати. Calibre показує лише файли TrueType і OpenType, які знайдено у вашій системі. Багато таких шрифтів можна отримати з інтернету. Просто збережіть файли .ttf/.otf і скопіюйте їх до теки Library/Fonts у вашій домашній теці.

Чому calibre не запускається у Windows?

Може бути декілька причин:

- Якщо повідомлень про помилки немає, але вікно calibre не з’являється, ймовірно, вікно з’явилося поза екраном. Ви можете зібрати усі вікна на поточному екрані за допомогою однієї з методик, які описано тут [https://www.wikihow.com/Bring-an-Off-Screen-Window-Back-on-Windows].

- Якщо ви бачите повідомлення про те, що calibre не може відкрити файл, оскільки файл використовується іншою програмою, виконайте такі дії:

- Вилучіть calibre

- Перезавантажте систему

- Знову встановіть calibre, але не запускайте calibre з вікна майстра для встановлення.

- Тимчасово вимкніть програму-антивірус (щоб захистити при цьому ваш комп’ютер, перед цим від’єднайте комп’ютер від інтернету).

- Зазирніть до теки, яку ви вибрали для вашої бібліотеки calibre. Якщо у ній ви бачите файл із назвою metadata.db, вилучіть його.

- Запустіть calibre

- З цього моменту ви маєте отримати змогу запускати calibre у звичний спосіб.

- Якщо система повідомляє вам про помилку, яку пов’язано із неочікуваним перериванням роботи функції Python, після оновлення calibre, спочатку вилучіть встановлену версію calibre, потім вилучіть такі теки (якщо вони існують) C:\Program Files\Calibre і C:\Program Files\Calibre2. Після цього повторно встановіть програму. Помилка має зникнути.

- Якщо ви бачите повідомлення про помилку під час роботи майстра початкового налаштовування при першому запуску calibre, спробуйте вибрати як теку бібліотеки C:\library (іноді у calibre виникають проблеми із теками бібліотеки, шляхи до яких містять нелатинські літери або лише цифри тощо).

- Спробуйте запустити програму від імені адміністратора (клацніть правою кнопкою на піктограмі і виберіть у контекстному меню пункт Запустити від імені адміністратора (Run as Administrator).

Якщо програма і далі відмовляється запускатися, відкрийте вікно командного рядка (натисніть клавішу Windows і клавішу R одночасно; потім введіть cmd.exe у діалоговому вікні запуску програм, яке буде відкрито). У вікні командного рядка введіть вказану нижче команду і натисніть клавішу Enter:

calibre-debug -g

Додайте виведені у довідковому повідомленні дані до допису на форумі [https://www.mobileread.com/forums/forumdisplay.php?f=166].

Чому calibre часом повисає або аварійно завершує роботу?

Існує декілька відомих причин:

- Нещодавно ви з’єднали з вашим комп’ютером зовнішній монітор або телевізор. У цьому випадку кожного разу, коли calibre відкриває якесь нове вікно, зокрема вікно редагування метаданих або діалогове вікно перетворення, воно з’являється на другому моніторі, де ви його не бачите, тому думаєте, що програма calibre «зависла». Від’єднайте другий монітор і перезапустіть calibre.

- Вказані далі програми, як нам повідомляли, призводять до аварійного завершення роботи calibre. Якщо ви запустили якусь з цих програм, завершіть її роботу до того, як запустите calibre або просто вилучіть пакунок із нею з системи. RoboForm, Logitech SetPoint Settings, Constant Guard Protection by Xfinity, Spybot, Killer Network Manager, Nahimic UI Interface, Acronis True Image.

- Ви користуєтеся USB-мишею або планшетом від Wacom. На жаль, драйвери Wacom і набір бібліотек для побудови графічного інтерфейсу calibre несумісні. Спробуйте скористатися мишею чи планшетом іншого виробника.

- У деяких 64-бітових версіях Windows працює програмне забезпечення для захисту або налаштовано параметри, які не дають 64-бітовій версії calibre працювати належним чином. Якщо ви користуєтеся 64-бітовою версією calibre, спробуйте перемкнутися на 32-бітову версію.

- Якщо аварійне завершення трапляється, коли ви намагаєтеся скопіювати текст з вікна програми для перегляду електронних книг calibre, найімовірнішою причиною є запущена програма, яка спостерігає за вмістом буфера обміну даними або керує ним. Вимкніть цю програму, і все має запрацювати.

- Якщо аварійне завершення трапляється, коли ви використовуєте діалогове вікно роботи з файлами, зокрема натискаєте кнопку Додати книги або кнопку Зберегти на диск, тоді на вашому комп’ютері встановлено якесь програмне забезпечення, яке перешкоджає роботі розширень командної оболонки. Серед відомих прикладів такого програмного забезпечення SpiderOak, odrive sync, Dell Backup and Recovery та NetDrive. Якщо встановлено одну з цих програм, вилучіть її пакунок, і усе запрацює. Ви також можете скористатися NirSoft Shell Extension Viewer [https://www.nirsoft.net/utils/shexview.html], щоб переглянути встановлені розширення оболонки і вимкнути окремі з них, якщо ви не хочете вилучати усю програму. Не забудьте перезапустити Провідник або перезавантажити систему після вимикання розширень оболонки.

Якщо жоден із описаних варіантів не спрацьовує, на вашому комп’ютері встановлено якусь іншу програму, яка заважає роботі calibre. Спочатку перезавантажте ваш комп’ютер у безпечному режимі, щоб у системі працювало якомога менше програм, і подивіться, чи не позбавить це вас від аварійних завершень. Якщо усе працює, проблемою точно є якась програма. Найімовірнішими кандидатами є програми, які намагаються змінити поведінку інших програм, зокрема антивіруси, драйвери пристроїв, щось схоже на RoboForm (програму для автоматичного заповнення форм) або програми для людей із обмеженими можливостями, зокрема програми голосового керування чи читання з екрана комп’ютера.

Єдиним способом виявити причину є припинення роботи програм одна за іншою з метою визначення тієї з них, яка спричиняє проблеми. Тобто слід діяти так: зупинити роботу програми, запустити calibre, перевірити, чи немає аварійних завершень роботи. Якщо аварійне завершення трапилося, зупинити роботу іншої програми і повторити процедуру.

Чому засіб для перегляду електронних книг calibre та засоби редагування книг не працюють у Windows?

У цих двох програмах використано апаратне прискорення обробки графіки, оскільки у них вбудовано версію браузера Chrome для показу HTML. Якщо програми не працюють, причиною є несумісність із драйверами до графічного процесора у вашій системі. Спробуйте оновити драйвери і перезавантажити систему. Якщо це не допоможе, ви можете встановити змінну середовища QTWEBENGINE_CHROMIUM_FLAGS у значення --disable-gpu, щоб вимкнути апаратне прискорення обробки. Докладніше про це можна дізнатися з цієї сторінки [https://doc.qt.io/qt-6/qtwebengine-debugging.html].

Використання засобу перегляду або виконання перетворень призводить до повідомлення про заборону доступу (permission denied) у Windows. Що робити?

Щось на вашому комп’ютері заважає calibre отримати доступ до власних тимчасових файлів програми. Ймовірно, маємо помилкові права доступу до вашої теки Temp. Перейдіть до теки file:C:\Users\USERNAME\AppData\Local у Провіднику Windows, клацніть правою кнопкою миші на теці file:Temp, виберіть пункт Властивості і перейдіть на вкладку Безпека. Переконайтеся, що ваш обліковий запис користувача має повний контроль над цією текою.

Деякими користувачами повідомлялося, що вказана нижче команда, віддана у командному рядку від імені адміністратора системи, виправила права доступу. Щоб відкрити командний рядок від імені адміністратора, пошукайте у меню «Пуск» cmd.exe, потім клацніть правою кнопкою миші на полі команди і виберіть Запустити від імені адміністратора. У вікні командного рядка впишіть вказану нижче команду і натисніть клавішу Enter:

icacls "%appdata%\..\Local\Temp" /reset /T

Крім того, ви можете запустити calibre від імені адміністратора. Втім, робота у такому режимі призводить до недоступності частини можливостей програми, зокрема перетягування зі скиданням позначок файлів.

Нарешті, деякі користувачі повідомляють, що вимикання UAC усуває проблему.

Чому calibre не запускається або аварійно завершує роботу у macOS?

Поширеною причиною аварійних завершень програми у macOS є використання технологій доступності, які несумісні із набором графічних бібліотек, які використовуються у calibre. Спробуйте вимкнути VoiceOver, якщо цю можливість увімкнено. Крім того, відкрийте сторінку Системні параметри  →  Система  →  Універсальний доступ і зніміть позначення з пункту уможливлення доступу до допоміжних пристроїв на усіх вкладках. Іншою причиною можуть бути деякі сторонні програми, які змінюють поведінку системи, зокрема Smart Scroll.

Ви можете отримати діагностичні дані щодо того, чому не вдається запустити calibre, запустивши Console.app. Діагностичні дані буде виведено до вікна цієї програми. Якщо у діагностичних даних міститься рядок, який є подібним до такого:

Qt: internal: -108: Error ATSUMeasureTextImage text/qfontengine_mac.mm

Це означає, що проблема, ймовірно, у пошкодженому кеші шрифтів. Щоб спорожнити кеш, скористайтеся цими настановами [https://www.macworld.com/article/1139383/fontcacheclear.html]. Якщо вони не допоможуть вирішити проблему, пошукайте у вашій системі пошкоджений файл шрифту у каталозі ~/Library/Fonts або іншому подібному каталозі. Найпростішим способом виявлення пошкоджених шрифтів у macOS є використання програми «Книга шрифтів». Просто позначте усі шрифти і виберіть у меню «Файл» пункт «Перевірити шрифти» («Validate fonts»).

Після запуску засобу перегляду книг calibre видно лише біле або чорне полотно. Що робити?

Причиною є несумісність між Qt WebEngine, бібліотекою, яку засіб перегляду використовує для обробки зображення, та драйверами графічного процесора у вашій системі. По-перше, спробуйте оновити драйвери до графічної картки. Якщо це не допомагає, можете спробувати вимкнути апаратне прискорення у Qt WebEngine встановленням для змінної середовища QTWEBENGINE_CHROMIUM_FLAGS значення --disable-gpu. Див. розділ Змінні середовища, щоб дізнатися більше про те, як змінювати змінні середовища.

Мною отримано програму для встановлення. Чому вона не спрацьовує?

Іноді під час отримання даних з інтернету відбувається пошкодження цих даних. Якщо отримана вами програма для встановлення calibre не запускається, спробуйте отримати її ще раз. Якщо повторне отримання програми не допомагає, спробуйте отримати її з альтернативного джерела [https://github.com/kovidgoyal/calibre/releases/latest]. Якщо програма для встановлення не запускається, щось на вашому комп’ютері заважає їй це зробити.

- Спробуйте тимчасово вимкнути програму-антивірус (Microsoft Security Essentials, Kaspersky, Norton або McAfee). Робота таких програм є найпоширенішою причиною повисання процесу оновлення у незавершеному стані.

- Так само, якщо засіб встановлення не може виконати завдання, і у вас запущено Microsoft PowerToys, завершіть їхню роботу.

- Спробуйте перезавантажити систему і запустіть програму для чищення регістру, наприклад Wise registry cleaner [https://www.wisecleaner.com].

- Спробуйте встановити програму наново. Це означає, вилучіть calibre, витріть каталог C:\Program Files\Calibre2 (або каталог, який ви раніше вибрали для встановлення calibre). Потім повторно встановіть calibre. Зауважте, що вилучення пакунка не призведе до вилучення ваших книг або параметрів роботи програми.

- Спробуйте отримати пакунок для встановлення за допомогою іншої програми для перегляду інтернету. Наприклад, якщо ви користувалися Microsoft Edge, спробуйте скористатися для отримання Firefox або Chrome.

- Якщо система повідомляє вам про нестачу DLL у Windows, найімовірніше, маєте помилкові права доступу до вашого каталогу тимчасових даних. Перейдіть до теки C:\Users\КОРИСТУВАЧ\AppData\Local у Провіднику Windows, клацніть правою кнопкою миші на теці Temp виберіть пункт Властивості і перейдіть на вкладку Безпека. Переконайтеся, що користувач вашого облікового запису має повний контроль над цією текою.

Якщо вам не вдається змусити працювати засіб встановлення програми у Windows, ви можете скористатися портативною версією calibre [https://calibre-ebook.com/uk/download_portable], яка не потребує встановлення (це просто архів ZIP із програмою).

Антивірусна програма стверджує, що calibre є вірусом або троянською програмою. Щоб робити?

По-перше, слід перевірити, чи отримали ви calibre з офіційного сайта [https://calibre-ebook.com/uk/download]. Переконайтеся, що ви натискали посилання для отримання програми ліворуч, а не пункти реклами праворуч. calibre — дуже популярна програма, тому шахраї намагаються створити сайти, які пропонують отримати програму, і вбудовують до коду програми шкідливий код.

Якщо ви отримали пакунок з програмою з офіційного джерела, а ваша антивірусна програма повідомляє, що calibre заражено вірусом, тоді ця антивірусна програма помиляється. У антивірусних програмах для пошуку вірусів використовують евристику, взірці коду, який «виглядає підозріливо». Це щось схоже за профілювання за кольором шкіри. Програма calibre є програмою з повністю відкритим кодом. Ви можете самі ознайомитися з кодом (або найняти когось, хто зробить це за вас) і переконатися, що у програмі немає вірусів. Будь ласка, повідомте про таку помилкову ідентифікацію програми як вірусу компанії, у якої ви придбали антивірусне програмне забезпечення. Якщо антивірусна програма не дає вам отримати або встановити calibre, тимчасово вимкніть її, встановіть calibre, а потім знову увімкніть.

Як створити резервну копію даних calibre?

Найважливішою є резервна копія теки бібліотеки calibre — у ній містяться усі книги і їхні метадані. Це тека, яку ви вибрали як теку бібліотеки calibre, коли запускали calibre вперше. Шлях до цієї теки можна визначити натисканням кнопки з піктограмою calibre на головній панелі інструментів. Вам слід зробити резервну копію усієї теки разом з усіма файлами та підтеками, які у ній зберігаються.

Ви можете перемкнути calibre на використання теки резервної копії бібліотеки простим натисканням кнопки з піктограмою calibre на панелі інструментів з наступним вибором теки резервної копії. У теці резервної копії зберігатимуться дані щодо нетипових стовпчиків та збережених пошуків, а також усі книги і їхні метадані.

Якщо ви хочете створити резервну копію налаштувань і додатків calibre, вам слід створити резервну копію теки налаштувань. Визначити розташування теки налаштувань можна за допомогою сторінки Налаштування  →  Різне. Зауважте, що підтримки відновлення даних з тек налаштувань офіційно не передбачено, але у більшості випадків таке відновлення має спрацювати. Достатньо просто скопіювати вміст теки резервної копії до поточного каталогу налаштувань, щоб відновити налаштування.

Як прочитати придбані книги EPUB у calibre (або що робити з файлами .acsm)?

У більшості книг EPUB реалізовано DRM. Calibre не може відкрити такі книги. Ви можете користуватися calibre для зберігання і передавання цих книг на ваш пристрій для читання електронних книг. Спочатку, вам слід уповноважити ваш пристрій для читання на комп’ютері Windows за допомогою Adobe Digital Editions. Після цього, книги EPUB, які передано за допомогою calibre, можна буде без проблем читати. Якщо ви придбаєте книгу epub на якомусь сайті, ви отримаєте файл «.acsm». Цей файл слід відкрити за допомогою Adobe Digital Editions — програми, яка отримає саму електронну книгу «.epub». Файл електронної книги буде збережено до теки «My Digital Editions», звідки ви можете додати його до бібліотеки calibre.

Бачу повідомлення «Доступ заборонено» («Permission Denied»). Що робити?

Повідомлення про заборону доступу може мати багато причин, жодна з яких не пов’язана із calibre.

- Ви можете отримати повідомлення щодо недостатні права доступу, якщо ви використовуєте картку SD із увімкненим захистом від запису.

- У macOS, якщо у вас виникають проблеми із правами доступу при з’єднанні пристрою із calibre, ви можете усунути їх за допомогою сторінки Налаштування системи > Безпека і конфіденційність > Конфіденційність > Файли і теки.

- Якщо ви або якась програма, якою ви користувалися, змінила права доступу до файлів на такі, що передбачають лише читання.

- Якщо на пристрої є помилка файлової системи, яка спричиняє монтування файлової системи операційною системою комп’ютера у режимі лише читання або позначення певного файла як придатного лише до читання, такого, що потребує виправлення.

- Якщо для файлів встановлено власника, який не є вашим користувачем.

- Якщо файл відкрито у іншій програмі.

- Якщо файл зберігається на пристрої, можливо перевищено обмеження у 256 файлів у кореневій теці пристрою. Якщо це так, вам доведеться форматувати пристрій або картку SD, щодо якої було повідомлено про помилку, з використанням файлової системи FAT32 або вилучити якісь файли з цієї картки SD або пристрою.

Перш ніж поновлювати користування calibre, вам слід виправити помилку, пов’язану із правами доступу. Уважно прочитайте повідомлення про помилку, подивіться, який саме файл там вказано і виправте права доступу до цього файла або теки, у якій він зберігається.

Як зробити, щоб метадані коментаря було показано на пристрої для читання?

На більшості пристроїв для читання не передбачено підтримки показу коментарів. Вам слід поскаржитися виробникові про це і сподіватися на те, що під вашим тиском та тиском інших користувачів ситуація зміниться. Тим часом, ви можете вставити метадані, зокрема коментарі, на сторінку суперобкладинки, за допомогою пункту Вставити метадані як сторінку на початку книги у вікні перетворення. Цей пункт розташовано у розділі Виявлення структури параметрів перетворення. Зауважте, що отримання цієї сторінки з метаданими можливе лише після перетворення книги. Якщо ваша книга вже зберігається у форматі, який не потребує перетворення, ви можете перетворити її без зміни формату.

Ще одним варіантом є створення каталогу у форматі електронної книги, де міститиметься список усіх книг у вашій бібліотеці calibre з їхніми метаданими. Для отримання доступу до інструмента створення каталогу наведіть вказівник миші на кнопку перетворення на панелі інструментів, натисніть ліву кнопку миші і трохи її потримайте. І, перш ніж ви могли б спитати, ні, не можна створювати каталог, який безпосередньо посилатиметься на книги на вашому пристрої для читання електронних книг.

Як налаштувати calibre на використання проксі-сервера HTTP?

Типово, calibre використовує параметри проксі-сервера, які визначено у вашій операційній системі на загальному рівні. Іноді ці параметри є помилковими, наприклад, у Windows, якщо ви не користуєтеся Microsoft Edge, параметри проксі-сервера можуть бути типовими або застарілими. Ви можете повідомити calibre про те, що слід використовувати певний проксі-сервер, встановленням змінних середовища http_proxy та https_proxy. Формат значення для змінної такий: http://користувач:пароль@назва_сервера. Правильне значення для цієї змінної можна дізнатися у адміністратора системи. Зауважте, що у calibre передбачено підтримку лише проксі-серверів HTTP, а не проксі-серверів SOCKS. Переглянути поточні параметри проксі-сервера можна за допомогою сторінки Налаштування  →  Різне.

Я хочу реалізувати якісь нові можливості у calibre. Як це зробити?

Маєте два варіанти:

1. Створіть латку, змінивши код calibre, і надішліть її розробнику для рецензування та включення до основного сховища. Див. розділ Розробка [https://calibre-ebook.com/uk/get-involved].

2. Відкрийте запит на реалізацію можливості [https://calibre-ebook.com/uk/bugs]. Пам’ятайте, що хоча вам може здатися, що можливість, яку ви просите реалізувати, є надзвичайно важливою, з цим можуть не погодитися розробники calibre. На щастя, calibre — програма з відкритим кодом. Це означає, що ви завжди можете реалізувати можливість власноруч або найняти когось, хто реалізує її за вас. Більше того, архітектуру calibre побудовано на додатках, отже можливість можна реалізувати у форматі додатка, див. розділ Створення власних додатків для розширення функціональних можливостей calibre.

Чому у calibre немає системи автоматичного оновлення?

Через багато причин:

- У оновленні щотижня немає потреби. Якщо у вас немає проблем із працездатністю calibre, вимкніть сповіщення щодо оновлення і не переймайтеся ним. Можете перевіряти наявність оновлень щороку або щомісяця. Пункт вимикання сповіщень щодо оновлення є у самому діалоговому вікні сповіщення.

- Обмін даними у процесі оновлення calibre на сьогодні складає близько 150 ТБ щомісяця [https://calibre-ebook.com/dynamic/downloads]. Реалізація автоматичного оновлення значно збільшить цю цифру і коштуватиме авторам програми тисячі доларів щомісяця, які комусь доведеться платити.

- Якщо розробники реалізують діалогове вікно для отримання оновлень і запуску програми для оновлення, замість відкриття сторінки сайта, як це робиться зараз, це збереже найпалкішим фанатам оновлення calibre не більше п’яти клацань кнопкою миші. У нас є набагато пріоритетніші завдання у розробці calibre.

- Якщо вам дуже-дуже не хочеться отримувати новий пакунок calibre щотижня, але хочеться користуватися найсвіжішим кодом, вам варто запускати програму з початкового коду, оскільки його оновлення є тривіальною справою. Настанови можна знайти тут.

- Існують сторонні засоби автоматичного оновлення calibre, які створено користувачами calibre. Посилання на них розміщено на форумі calibre [https://www.mobileread.com/forums/forumdisplay.php?f=238].

Якими є умови ліцензування calibre?

Програму calibre випущено за умов дотримання GNU General Public License v3 (ліцензії для програм з відкритим кодом). Це означає, що ви можете вільно розповсюджувати calibre, якщо залишатимете загальнодоступним початковий код програми. Отже, якщо ви хочете розмістити calibre на компакт-диску з вашим продуктом, вам також слід розмістити на компакт-диску початковий код програми. Початковий код можна отримати тут [https://download.calibre-ebook.com]. Ви можете безкоштовно користуватися результатами перетворення даних за допомогою calibre у будь-який бажаний для вас спосіб. Ви не можете використовувати код або бібліотеки з calibre у вашому програмному забезпеченні, не зробивши його код відкритим. Докладніше про це у тексті GNU GPL v3 [https://www.gnu.org/licenses/gpl.html].

Як запустити calibre з флешки?

Портативну версію calibre можна отримати тут [https://calibre-ebook.com/uk/download_portable].

Як скористатися частинами calibre, зокрема засобом отримання новин та сервером даних, на сервері під керуванням Linux?

По-перше, вам слід встановити calibre на вашому сервері під керуванням Linux. Якщо на вашому сервері встановлено сучасний дистрибутив Linux, у вас не повинно бути проблем зі встановленням calibre.

Примітка

Для роботи calibre потрібні бібліотеки GLIBC >= 2.31 і libstdc++ >= 6.0.28. Якщо ви маєте справу із застарілим сервером, вам доведеться або зібрати ці бібліотеки з початкових кодів, або скористатися calibre 3.48, які потрібна GLIBC >= 2.17, чи calibre 2.85.1, якій потрібна GLIBC >= 2.13, чи calibre версії 1.48, для якої достатньо GLIBC >= 2.10. Крім того, хоча для роботи інструментів командного рядка calibre не потрібен графічний сервер, деяким з них потрібні встановлені у вашій системі бібліотеки графічного сервера. Причиною є використання Qt (бібліотеки, яка використовується для виконання різноманітних завдань, які пов’язано із обробкою зображень) та компонування частин програми з цими бібліотеками. Якщо ви отримуєте повідомлення ImportError щодо якихось модулів Qt, ймовірно, у системі не вистачає деяких бібліотек сервера X (графічного сервера). Типовими кандидатами є такі: libxcb-cursor0, libxcb-xinerama0, libegl1, libopengl0.

Запустити сервер calibre можна за допомогою такої команди:

/opt/calibre/calibre-server /path/to/the/library/you/want/to/share

Отримати новини і перетворити їх дані на електронну книгу можна за допомогою такої команди:

/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub

Якщо ви хочете створити файл MOBI, скористайтеся outputfile.mobi і параметром --output-profile kindle.

Надіслати отримані новини електронною поштою можна за допомогою такої команди:

/opt/calibre/calibre-smtp

Читач може сам повправлятися у визначення точного вигляду рядка команди.

Нарешті, ви можете додати отримані новини до бібліотеки calibre ось так:

/opt/calibre/calibredb add --with-library /path/to/library outfile.epub

Не забудьте ознайомитися із розділом Інтерфейс командного рядка документації, щоб дізнатись більше про ці та інші команди.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники

Навчальні посібники

У цьому розділі наведено навчальні посібники, які допоможуть вам розпочати користуватися додатковими можливостями calibre, зокрема XPath та шаблонами.

- Додавання вашого улюбленого сайта новин

- Керування підгрупами книжок, наприклад, «жанр»

- Підручник з XPath

- Мова шаблонів calibre

- Все про використання формальних виразів в calibre

- Створення власних додатків для розширення функціональних можливостей calibre

- Формули в електронних книгах
- Створення каталогів AZW3 • EPUB • MOBI
- Віртуальні бібліотеки

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »

- Керування підгрупами книжок, наприклад, «жанр»

Керування підгрупами книжок, наприклад, «жанр»

Іноді виникає потреба в упорядковуванні книг у бібліотеці за підгрупами, подібними до підтек. Найпоширенішою причиною цього є створення ієрархій жанрів, але можуть бути і інші причини. Комусь потрібен спосіб упорядкувати конспекти за темами і номерами курсів. Комусь хочеться стежити за подарунками за темою та отримувачем. У цьому розділі ми наведемо приклад поділу за жанрами.

Перш ніж продовжувати опис, варто зауважити, що ми не говоримо про теки на диску. Підгрупи не є теками з файлами. Книги нікуди не копіюватимуться. Структуру файлів бібліотеки calibre не буде змінено. Замість цього, ми наведемо спосіб упорядковування і показу підгруп книг у бібліотеці calibre.

- Налаштовування

- Пошук

- Обмеження

- Корисні шаблонні функції

Загальними вимогами до підгруп, зокрема жанрів є такими:

- Підгрупа (наприклад жанр) має містити (вказувати на) книги, а не категорії книг. Це відрізняє підгрупи від категорій користувачів calibre.

- Книга може перебувати у декількох підгрупах (жанрах). Це відрізняє підгрупи від фізичних тек файлів.

- Підгрупи (жанри) мають складати ієрархію; підгрупи можуть містити інші підгрупи.

Мітки можуть дати вам перші два пункти. Якщо ви позначите міткою жанру книгу, ви зможете скористатися панеллю перегляду міток (або пошуком) для отримання списку книг відповідного жанру, тобто задовольнити вимоги першого пункту. Багато книг можуть мати однакові мітки, і це дає нам другий пункт. Проблема полягає у тому, що мітки не можуть задовольнити третій пункт. У них не передбачено ієрархічної структури.

Можливість створення ієрархій у calibre дасть вам третє — можливість бачити жанри у форматі «дерева» і можливість простого пошуку книг у жанрі або піджанрі. Наприклад, припустімо, що у вас така структура жанрів:

Genre
. History
.. Japanese
.. Military
.. Roman
. Mysteries
.. English
.. Vampire
. Science Fiction
.. Alternate History
.. Military
.. Space Opera
. Thrillers
.. Crime
.. Horror
etc.

За допомогою ієрархічної структури ви зможете переглядати ці жанри на панелі перегляду міток у формі дерева, як це показано на знімку вікна. У нашому прикладі зовнішній рівень (Жанр) є нетиповим стовпчиком, що містить дані щодо жанру. У жанрах містяться піджанри, пункти яких позначено невеличким трикутничком. Натискання цього трикутничка призведе до відкриття списку з пунктами піджанрів, як це можна бачити для жанрів «History» та «Science Fiction».

Натисканням пункту жанру ви можете розпочати пошук усіх книг відповідного жанру або дочірніх записів цього жанру. Наприклад, натискання пункту «Science Fiction» відкриє усі три дочірні пункти жанрів, «Alternate History», «Military» та «Space Opera». Натискання пункту «Alternate History» відкриє список тих книг цього жанру, які не належать до жанрів «Military» або «Space Opera». Звичайно ж, книга може належати до декількох жанрів. Якщо книга належить одразу до жанрів «Space Opera» та «Military», ви побачите пункт цієї книги у списках обох піджанрів. Обговорення можливості пошуку книг наведено нижче.

Іншою річчю, яку ви можете зауважити з зображення, є те, що жанр «Military» показано у списку двічі: одного разу під жанром «History» і ще таз під жанром «Science Fiction». Оскільки жанри перебувають у ієрархії, це два окремих жанри. Книга може належати до одного жанру, до іншого жанру або до обох жанрів (у цьому випадку сумнівно). Наприклад, книга Вінстона Черчілля «Друга світова війна» може бути належати до жанру «History.Military». Книги Девіда Вебера з циклу про Гонор Гаррінґтон можуть належати до жанру «Science Fiction.Military» і, у цьому випадку, до жанру «Science Fiction.Space Opera».

Щойно буде створено запис жанру, до якого належатиме принаймні одна книга, ви зможете дуже просто застосувати цей жанр до інших книг: достатньо перетягнути позначки цих книг із панелі бібліотеки на пункт жанру, до якого належать відповідні книги. Ви також можете прив’язувати книги до жанрів у редакторі метаданих. Докладніше про це нижче.

Налаштовування

Тепер, у вас може виникнути питання: «Як же усе це налаштувати?» У три кроки: 1) створити нетиповий стовпчик, 2) повідомити calibre, що новий стовпчик слід вважати ієрархією і 3) додати жанри.

Створити нетиповий стовпчик можна у звичний спосіб, за допомогою пункту Налаштування  →  Додати власний стовпчик. У нашому прикладі використано «#genre» як назву пошуку і «Genre» як заголовок стовпчика. Типом стовпчика вибрано «Текст, відокремлений комами, зокрема мітки, який буде показано на панелі перегляду міток».

Далі, після перезапуску calibre вам слід повідомити calibre, що стовпчик слід вважати ієрархією. Відкрийте сторінку Налаштування  →  Зовнішній вигляд  →  Переглядач міток і введіть назву пошуку «#genre» до поля «Категорії з ієрархічними пунктами». Натисніть кнопку Застосувати для завершення налаштовування.

Зараз у стовпчику немає жодного жанру. Нам залишився один крок: слід застосувати жанр до книги. Жанру не існує у списку calibre, доки до нього не належатиме хоч одна книга. Щоб навчитися застосовувати жанр вперше, нам слід познайомитися з тим, як жанр виглядає у метаданих книги.

Ієрархія «речей» будується створенням запису, що складається з фраз, відокремлених крапками. Продовжуючи приклад із жанрами, цими записами могли б бути «History.Military», «Mysteries.Vampire», «Science Fiction.Space Opera» тощо. Отже, щоб створити запис жанру, ви вибираєте книгу, яка має належати до цього жанру, редагуєте метадані і вводите назву нового жанру до створеного вами стовпчика. У продовження нашого прикладу, якщо ви хочете пов’язати новий жанр «Comics» із піджанром «Superheroes» із книгою, вам слід натиснути кнопку Редагувати метадані для цієї книги (коміксу), вибрати вкладку Нетипові метадані, а потім ввести «Comics.Superheroes», як це показано нижче (інші нетипові стовпчики слід проігнорувати):

Після виконання вказаних вище дій ви побачите таке на панелі перегляду міток:

Після цього, щоб застосувати цей новий жанр до книги (наприклад, коміксу), ви можете або перетягнути позначку книги на пункт жанру, або додати жанр до книги за допомогою редагування метаданих у той самий спосіб, як це було зроблено вище.

Примітка

Ієрархічний перегляд працює, лише якщо на панелі навігації мітками встановлено режим упорядковування записів за назвою. Цей режим є типовим, його можна встановити натисканням кнопки Налаштувати у нижній частині панелі навігації мітками.

Пошук

Найпростішим способом пошуку за жанрами є використання панелі перегляду міток: достатньо просто натиснути пункт жанру, книги з якого ви хочете переглянути. Натискання пункту з дочірніми жанрами призведе до показу книг, що належать до жанру і усіх його піджанрів. Втім, це може призвести до певних питань. Лише через те, що запис жанру має дочірні записи, запис жанру не є сам жанром. Наприклад, книга може належати до жанру «History», але не належати до жанру «History.Military». Як же знайти книги, які належать лише до жанру «History»?

Механізм пошуку на панелі перегляду міток може визначати, чи є у запису дочірні записи. Якщо такі записи є, натисканням відповідного пункту виконується 5 пошуків, замість звичних 3. Першим є звичайний пошук за зеленим «плюсом», який знайде книги, які належать лише до жанру, пункт якого натиснуто (наприклад, до жанру «History»). Другим є подвійний «плюс» (показано вище), який знайде книги, які належать до жанру і усіх його піджанрів (наприклад, до жанру «History» і піджанру «History.Military»). Третім є звичайний червоний «мінус», який знайде усі книги поза жанрами. Четвертим є подвійний «мінус», який знайде книги поза жанрами і піджанрами. П’ятим є повернення до початку, без позначки, без пошуку.

Обмеження

Якщо ви шукаєте книги за жанром, а потім створюєте збережений пошук для результатів, ви можете скористатися полем «обмежити до», щоб створити віртуальну бібліотеку для відповідного жанру. Це корисно, якщо ви плануєте виконувати інші пошуки у межах жанру або хочете змінити або оновити метадані книг, що належать до певного жанру. Продовжуючи наш приклад, ви можете створити збережений пошук із назвою «History.Japanese», спочатку натиснувши пункт жанру «Japanese» на панелі перегляду міток, щоб перенести пошук до поля пошуку, потім ввівши «History.Japanese» до поля збереженого пошуку і натиснувши кнопку «Зберегти пошук» (зелене поле із білим «плюсом», яке розташовано праворуч).

Після створення збереженого пошуку ви можете використовувати його як обмеження.

Корисні шаблонні функції

У вас може виникнути бажання скористатися даними щодо жанру у шаблоні, зокрема шаблоні збереження на диск або шаблоні надсилання на пристрій. Тоді може виникнути питання: «Як отримати назву або назви жанрів найвищого рівня?». Для полегшення цього завдання передбачено шаблонну функцію calibre, subitems.

Наприклад, припустімо, що ви хочете додати найзовнішніший рівень жанрів до шаблону save-to-disk, щоб створити теки жанрів у такому форматі: «History/The Gathering Storm - Churchill, Winston». Для цього вам доведеться видобути перший рівень з ієрархії і додати його на початку разом із похилою рискою, яка позначатиме потребу у створенні теки. Наведений нижче шаблон впорається з цим завданням:

{#genre:subitems(0,1)||/}{title} - {authors}

Див. Мова шаблонів, щоб дізнатися більше про шаблони та функцію subitems().

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Підручник з XPath

Підручник з XPath

У цій частині підручника ми наведемо вступні зауваження щодо XPath [https://en.wikipedia.org/wiki/XPath], мови запитів, якою можна скористатися для вибору довільних частин документів HTML [https://en.wikipedia.org/wiki/HTML] у calibre. XPath є широковживаним стандартом, отже, пошук у мережі надасть вам дуже багато інформації щодо неї. Втім, увагу у цій частині підручника зосереджено на використанні XPath для завдань, пов’язаних із електронними книгами, зокрема пошуку заголовків глав у неструктурованому документі HTML.

Зміст

- Вибір за назвою теґу

- Вибір за атрибутами

- Вибір за вмістом теґу

- Зразок книги

- Вбудовані функції XPath

Вибір за назвою теґу

Найпростішою формою вибору є вибір теґів за назвою. Наприклад, припустімо, що вам хочеться вибрати усі теґи <h2> у документі. Запит XPath для цього буде дуже простим:

//h:h2 (Selects all <h2> tags)

Префікс «//» означає шукати на усіх рівнях документа. Тепер, припустімо, що ви хочете знайти теґи <span>, які перебувають всередині теґів <a>. Зробити це можна ось так:

//h:a/h:span (Selects <span> tags inside <a> tags)

Якщо ви хочете шукати теґи на певному рівні у документі, змініть префікс:

/h:body/h:div/h:p (Selects <p> tags that are children of <div> tags that are
children of the <body> tag)

Буде знайдено лише <p>A very short ebook to demonstrate the use of XPath.</p> у Зразок книги, але не буде знайдено жодного іншого теґу <p>. Префікс h: у наведених вище прикладах потрібен для встановлення відповідності теґам XHTML. Він потрібен, тому що на внутрішньому рівні calibre представляє усі дані як XHTML. У теґів XHTML є власний простір назв, і h: є префіксом простору назв для теґів HTML.

Тепер, припустімо, вам потрібно вибрати одразу теґи <h1> і теґи <h2>. Для виконання цього завдання нам знадобиться конструкція XPath, яка називається предикатом. предикат є простою перевіркою, яка використовується для вибору теґів. Перевірки можуть мати довільну потужність. У межах цього розділу ми наведемо і потужніші приклади. Предикат створюється взяттям виразу перевірки у квадратні дужки:

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

У цьому виразі XPath є декілька нових особливостей. Першою є використання замінника *. Цей замінник означає знайти будь-який теґ. Далі, зверніть увагу на вираз перевірки name()='h1' or name()='h2'. name() є прикладом вбудованої функції. Ця функція просто повертає назву теґу. Отже, за її допомогою ми можемо вибрати теґи, назвами яких є h1 або h2. Зауважте, що функція name() ігнорує простори назв, отже, у додаванні префікса h: немає потреби. У XPath є декілька корисних вбудованих функцій. Ще декілька буде представлено у цьому розділі.

Вибір за атрибутами

Для вибору тегу на основі його атрибутів слід користуватися предикатами:

//*[@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")

Тут оператор @ посилається на атрибути теґу. Ви можете скористатися деякими з вбудованих функцій XPath для встановлення складних відповідностей із значеннями атрибутів.

Вибір за вмістом теґу

За допомогою XPath ви навіть можете вибирати теґи на основі тексту, який у них міститься. Найкращим способом це зробити є використання потужності формальних виразів, тобто вбудованої функції re:test():

//h:h2[re:test(., 'chapter|section', 'i')] (Selects <h2> tags that contain the words chapter or
section)

Тут оператор . посилається на вміст теґу, так само, як оператор @ посилається на його атрибути.

Зразок книги

<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

name()

Назва поточного теґу.

contains()

contains(s1, s2) повертає «true», якщо s1 містить s2.

re:test()

re:test(джерело, взірець, прапорці) повертає true, якщо рядок джерело відповідає формальному виразу взірець. Одним із особливо корисних прапорців є i, прапорець, який вимикає врахування регістру символів під час порівняння. Чудовий підручник із синтаксису формальних виразів можна знайти на сторінці синтаксису формальних виразів [https://docs.python.org/library/re.html]

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Мова шаблонів calibre

Мова шаблонів calibre

Мова шаблонів calibre є специфічною для calibre мовою програмування, яка використовується у різних компонентах calibre. Її використовують для визначення шляхів до файлів, значення форматування та обчислення значень визначених користувачем стовпчиків. Приклади:

- Визначення структури каталогів та назви файла при зберіганні файлів з бібліотеки calibre на диск або на пристрій для читання книг.

- Визначення правил додавання піктограм і кольорів до списку книг calibre.

- Визначення «віртуальних» стовпчиків, які містять дані з інших стовпчиків.

- Розширені можливості пошуку у бібліотеці.

- Розширені можливості з пошуку і заміни у метаданих.

Мову побудовано навколо поняття «шаблон». Шаблон задає, які метадані книги слід використати, обчислення над цими метаданими, і як їх слід форматувати.

Базові шаблони

Базовий шаблон містить один або декілька виразів шаблонів. Вираз шаблона складається з тексту і назв у фігурних дужках ({}), які згодом буде замінено на відповідні метадані із оброблюваної книги. Наприклад, типовим шаблоном, який використано для збереження книг на пристроях у calibre, містить 4 шаблонних вирази:

{author_sort}/{title}/{title} - {authors}

Для книги «The Foundation» автора «Isaac Asimov» ми отримаємо:

Asimov, Isaac/The Foundation/The Foundation - Isaac Asimov

Символи похилих рисок не є шаблонними виразами, оскільки їх обмежено {}. Такі фрагменти тексту після обробки лишаються на місці. Наприклад, якщо шаблоном є такий:

{author_sort} Some Important Text {title}/{title} - {authors}

для «The Foundation» шаблон дасть:

Asimov, Isaac Some Important Text The Foundation/The Foundation - Isaac Asimov

Шаблон може отримувати доступ до усіх метаданих, які доступні у calibre, зокрема нетипових стовпчиків (стовпчиків, які створено користувачем), за їхньою назвою для пошуку. Щоб визначити назву для пошуку для стовпчика (який іноді називають «полем»), наведіть вказівник миші на заголовок стовпчика у списку книг calibre. У назвах для пошуку нетипових стовпчиків першим символом завжди є #. Для стовпчиків типу циклів передбачено додаткове поле із назвою '#назва_для_пошуку'_index, яке містить індекс у циклі. Отже, якщо у вас є нетипове поле циклу із назвою #myseries, буде також і поле із назвою #myseries_index. Індекс стандартного стовпчика циклу має назву series_index.

Окрім полів, заснованих на вмісті стовпчиків, ви також можете використовувати:

- {formats} — список форматів, доступних у бібліотеці calibre для книги

- {identifiers:select(isbn)} — ISBN книги

Якщо метаданих поля певної книги не визначено, поле у шаблоні буде замінено на порожній рядок (''). Приклад:

{author_sort}/{series}/{title} {series_index}

Якщо книга Азімова «Second Foundation» є частиною циклу «Foundation», шаблон дасть:

Asimov, Isaac/Foundation/Second Foundation 3

Якщо для книги не введено циклу, шаблон дасть:

Asimov, Isaac/Second Foundation

Обробник шаблонів автоматично вилучає послідовності похилих рисок і початкові та завершальні пробіли.

Додаткові можливості з форматування

За допомогою шаблонів, окрім вставляння метаданих, можна виконувати набагато ширший спектр дій. Шаблони можуть включати додатковий текст, якщо виконуються певні умови, і керувати тим, як буде форматовано підставлені дані.

Умовне включення тексту

Іноді може виникнути потреба у виведенні тексту, лише якщо певне поле є непорожнім Типовим прикладом є series і series_index. У цьому прикладі нам потрібен буде або порожній рядок або два значення, які відокремлено дефісом. У calibre для обробки подібних випадків передбачено спеціальний синтаксис запису шаблона.

Наприклад, використовуючи наведений вище приклад із «Foundation», припустімо, вам потрібно, щоб застосування шаблона давало таке: Foundation - 3 - Second Foundation. Цей шаблон дає такий результат:

{series} - {series_index} - {title}

Втім, якщо книга не належить до циклу, шаблон виведе - - заголовок Ймовірно, це зовсім не те, що потрібно. Здебільшого, бажаним результатом буде просто заголовок, без зайвих дефісів. Для отримання такого результату слід скористатися такою синтаксичною конструкцією шаблонів:

{field:|текст_префікса|текст_суфікса}

Цей шаблонний вираз визначає, що якщо field має значення XXXX, результатом буде prefix_textXXXXXsuffix_text. Якщо field є порожнім (не має значення), результатом буде порожній рядок (нічого), оскільки префікс і суфікс буде проігноровано. Префікс і суфікс можуть містити пробіли.

Не використовуйте підлеглі шаблони (`{ … }`) або функції (див. нижче) у префіксі або суфіксі.

За допомогою цієї синтаксичної конструкції ми можемо розв’язати поставлену вище задачу з шаблоном:

{series}{series_index:| - | - }{title}

Дефіси буде включено, лише якщо для книги визначено індекс у циклі, а таке трапляється, лише якщо метадані містять запис циклу. Продовжуючи приклад із «Foundation», шаблон дасть Foundation - 1 - Second Foundation.

Нотатки:

- Вам слід включити двокрапку після назви для пошуку, якщо ви хочете скористатися префіксом або суфіксом.

- Вам слід або не використовувати символи |, або використовувати обидва символи; користуватися одним з них, як у {field:| - }, не можна.

- Можна не вказувати текст для префікса або суфікса, якось так: {series:|| - }. Шаблон {title:||} є еквівалентним до шаблона {title}.

Форматування

Припустімо, вам потрібно переконатися, що series_index завжди записуватиметься у форматі з трьох цифр із початковими нулями. Ось як цього досягти:

{series_index:0>3s} — три цифри із початковими нулями

Для кінцевих нулів скористайтеся такою конструкцією:

{series_index:0<3s} — три цифрви із завершальними нулями

Якщо ви використовуєте індекси у циклі із проміжними значеннями (наприклад, 11), вам може знадобитися вирівнювання десяткової крапки. Наприклад, бажаним буде запис індексів 1 і 2.5 як 01.00 і 02.50 для належного упорядковування на пристроях із лексичним упорядкуванням. Щоб досягти потрібного результату, скористайтеся таким шаблоном:

{series_index:0>5.2f} — п’ять символів складаються із двох цифр і початковими нулями, десяткової крапки і 2 цифр після десяткової крапки.

Якщо вам потрібні лише перші дві літери даних, скористайтеся такою конструкцією:

{author_sort:.2} — лише перші дві літери імені у впорядкованому списку авторів

Більша частина форматування мови шаблонів calibre є похідною від Python. Щоб дізнатися більше про синтаксичні конструкції таких дій з форматування, ознайомтеся із документацією з Python [https://docs.python.org/3/library/string.html#formatstrings].

Використання шаблонів для визначення нетипових стовпчиків

Шаблони використовують для показу даних, яких немає у метаданих calibre або показу даних у форматуванні, яке відрізняється від стандартного форматування calibre. Наприклад, у вас може виникнути потреба бачити ISBN, поле, вміст якого calibre не показує. Для показу таких даних ви можете скористатися нетиповими стовпчиками. Створіть стовпчик із типом «Стовпчик, створений на основі інших стовпчиків» (надалі ми такі стовпчики називатимемо складеними) і введіть до нього шаблон. Результат: calibre показуватиме у стовпчику результат обчислення шаблона. Для показу ISBN створіть стовпчик і введіть {identifiers:select(isbn)} у поле шаблона. Щоб створити стовпчик, який міститиме значення двох нетипових стовпчиків циклів, скористайтеся шаблоном {#series1:||,}{#series2}.

Складені стовпчики можуть використовувати будь-які варіанти шаблонів, включно з форматуванням.

Зауваження: редагувати дані, які показано у складеному стовпчику не можна. Редагувати слід дані у стовпчиках-джерелах. Якщо ви спробуєте виконати редагування складеного стовпчика, наприклад, подвійним клацанням на даних у ньому, calibre відкриє для редагування шаблон, а не дані, які він виводить.

Шаблони і набори метаданих

Набори метаданих використовуються для внесення змін до метаданих, що записуються до книг під час дій із надсилання на пристрій і збереження на диск. Набір метаданих надає вам змогу вказати шаблон для надання даних, які слід записати до метаданих книги. Ви можете скористатися наборами метаданих для внесення змін до таких полів: authors, author_sort, language, publisher, tags, title, title_sort. Набори метаданих допомагають у використанні спеціалізованих метаданих для книг на пристрої з метою усування проблем із упорядковуванням книг або їхнім показом.

Під час створення набору метаданих ви визначаєте формат і пристрій, для яких використовуватиметься набір. Передбачено особливий пристрій, save_to_disk, який використовується для збереження книг у певних форматах (на відміну від надсилання книг на пристрій). Після вибору формату і пристрою вам слід вибрати поля метаданих для внесення змін і надати шаблони для визначення нових значень цих полів. Ці шаблони буде з’єднано з відповідними полями призначення. Ви, звичайно ж, можете використовувати у шаблонах наборів метаданих складені стовпчики.

Набори метаданих є доволі гнучкими — їх можна писати у режимі однієї функції, режимі програмування шаблонів, загальному режимі програмування та режимі шаблонів Python.

Якщо до метаданих може бути застосовано набір (на сервері даних, для збереження на диск або надсилання на пристрій), calibre шукає визначені набори метаданих і вибирає належний для заданого формату і пристрою. Наприклад, для того, щоб знайти належний набір метаданих для книги EPUB, яку надсилають на пристрій під керуванням ANDROID, calibre шукає набори метаданих у такому порядку:

- набір метаданих із точною відповідністю за форматом і пристроєм, наприклад EPUB і ANDROID

- набір метаданих із точним відповідником за форматом і спеціальним варіантом any device, наприклад EPUB і any device

- набір метаданих із спеціальним варіантом any format і точною відповідністю за пристроєм, наприклад any format і ANDROID

- набір метаданих any format і any device

Поля міток (tags) і авторів (authors) обробляються у особливий спосіб, оскільки у обох цих полях може міститися декілька записів. У книги може бути багато міток і багато авторів. Якщо ви визначаєте одне з цих двох полів як таке, вміст якого слід змінити, результати використання шаблона буде перевірено на наявність декількох записів. Для міток результат буде розділено на записи за знайденими calibre комами. Наприклад, якщо шаблон створює значення Трилер, Жахи, результатом буде дві мітки, Трилер і Жахи. Способів додавання коми у окрему мітку не передбачено.

Те саме стосується авторів, але для поділу на записи використовується інший символ, & (амперсанд), а не кома. Наприклад, якщо шаблоном створюється значення Blogs, Joe&Posts, Susan, для книги буде визначено двоє авторів, Blogs, Joe і Posts, Susan. Якщо ж шаблон створює значення Blogs, Joe;Posts, Susan, книга матиме один запис автора із доволі чудернацьким іменем.

Набори метаданих впливають на метадані, які записуються до книги, коли її зберігають на диск або записують на пристрій для читання. Набори метаданих не змінюють метадані, які використовуються діями зберегти на диск та надіслати на пристрій для створення назв файлів. Назви файлів будуються за допомогою шаблонів, які вказано у відповідних місцях вікна налаштувань.

Використання функцій у шаблонах — режим єдиної функції

Припустімо, вам потрібно показати значення поля літерами верхнього регістру, хоча зазвичай вміст цього поля записується регістром символів як у заголовку. Досягти цього можна за допомогою функцій шаблонів. Наприклад, для переведення літер у верхній регістр можна скористатися функцією uppercase, ось так: {title:uppercase()}. Для показу назви у форматі, типовому для заголовків, скористайтеся шаблоном {title:titlecase()}.

Функції вставляють у частину форматування шаблона Їх вписують після : і перед першим | або завершальним }, якщо не використано префікса або суфікса. Якщо вам потрібне одразу форматування і функції, функцію включають після ще одного :. Функції повертають значення стовпчика, вказаного у шаблоні, належним чином зміненим.

Синтаксична конструкція для використання функцій є однією з таких:

{lookup_name:function(arguments)}
{lookup_name:format:function(arguments)}
{lookup_name:function(arguments)|prefix|suffix}
{lookup_name:format:function(arguments)|prefix|suffix}

За назвами функцій завжди мають іти початкова і завершальна круглі дужки. Деяким функціям обов’язково слід передавати додаткові значення (аргументи), і ці аргументи слід вказувати у дужках. Аргументи відокремлюють комами. Самі коми (коми як текст, а не роздільник аргументів) слід екранувати символом зворотної похилої риски (\) . Останній (або єдиний) аргумент не може містити текстової завершальної дужки.

Функції обчислюють до специфікацій форматів та префіксів/суфіксів. Нижче наведено приклад одночасного використання форматування та функції.

Важливе зауваження: якщо ви маєте досвід програмування, будь ласка, зауважте, що синтаксис у цьому режимі (режимі єдиної функції) не зовсім такий, як ви очікуєте Для рядків не потрібні лапки. Пробіли мають значення. Усі аргументи мають бути сталим; проміжних обчислень не передбачено.

Не використовуйте підлеглих шаблонів (`{ … }`) у аргументах функцій. Замість них, використовуйте режим шаблонного програмування та загальний режим програмування.

У деяких функціях використовуються формальні вирази. У мові шаблонів при встановленні відповідності формальних виразів регістр символів не береться до уваги.

У наведеній нижче документації щодо функцій позначення [щось]* означає, що щось може бути повторено нуль або більшу кількість разів. Позначення [щось]+ означає, що щось повторюється один або декілька разів (але має бути принаймні один такий запис).

Функціями, які призначено для використання у режимі єдиної функції, є такі:

capitalize() – повертає значення поля так, що кожне слово у результаті починатиметься з великої літери, а решта літер слів будуть малими.

contains(значення, взірець, текст для відповідності, текст для невідповідності) — перевіряє, чи міститься у значенні відповідник формального виразу взірець. Повертає текст для відповідності, якщо буде знайдено відповідник, інакше повертає текст для невідповідності.

count(роздільник) — за заданим значенням списком пунктів, відокремлених роздільником роздільник, повертає кількість пунктів у списку. У більшості списків як роздільник використовується кома, для авторів може використовуватися амперсанд. Приклади: {tags:count(,)}, {authors:count(&)}. Варіанти: count(), list_count()

format_number(шаблон) — вважати значення числом і форматувати число на основі шаблону форматування python, наприклад {0:5.2f}, {0:,d} або ${0:5,.2f}. Шаблон форматування має починатися з {0: і завершуватися }, як у наведених вище прикладах. Виключення: ви можете обійтися без початкового «{0:» і завершального «}», якщо у шаблоні форматування міститься лише один формат. З докладнішими прикладами можна ознайомитися за допомогою документації до мови шаблонів та документації з Python [https://docs.python.org/3/library/string.html#formatstrings]. Повертає порожній рядок, якщо виконати форматування не вдасться.

human_readable() — очікує, що значення є числом, і повертає рядкове представлення числа у кілобайтах, мегабайтах, гігабайтах тощо.

ifempty(текст для порожнього) — якщо значення не є порожнім, повертає значення поля, інакше повертає текст для порожнього.

in_list(роздільник, [ взірець, значення_якщо_знайдено, ]* значення_якщо_не_знайдено) — обробляє значення як список пунктів, відокремлених роздільником, порівнює взірець з кожним зі значень у списку Якщо рядок збігається зі значенням, повертає значення_якщо_знайдено, інакше повертає значення_якщо_не_знайдено. Значення взірець і значення_якщо_знайдено можна повторювати довільну кількість разів, залежно від значення запису. Перевірка взірців виконується послідовно. Функція повертає перший знайдений відповідник.

language_strings(локалізація) — повертає назви мов [https://www.loc.gov/standards/iso639-2/php/code_list.php] для кодів мовs [https://www.loc.gov/standards/iso639-2/php/code_list.php], які передано як значення. Приклад: {languages:language_strings()}. Якщо локалізація дорівнює нулеві, повертає рядки англійською. Якщо локалізація не дорівнює нулеві, повертає рядки мовою поточної локалі. Значення коди_мов є списком значень, які відокремлено комами.

list_item(номер, роздільник) — обробляє значення як список пунктів, відокремлених роздільником «роздільник», повертає пункт з номером «номер». Перший пункт має номер нуль. Останній з пунктів має індекс ``-1, як у «list_item(-1, роздільник)». Якщо пункту немає у списку, буде повернуто порожній рядок.

lookup([ взірець, ключ, ]* інший_ключ) — буде виконано встановлення відповідності значення взірцям за порядком. Якщо взірець є відповідним, буде повернуто значення поля, яке вказано назвою key. Якщо взірець не є відповідним, буде повернуто значення поля інший_ключ. Див.``switch`` (нижче).

lowercase() — повертає значення поля малими літерами.

rating_to_stars(використання_пів_зірок) — повертає оцінку як рядок з символів зірок (★). Значення має бути числом від 0 до 5. Встановіть значення використання_пів_зірок рівним 1, якщо ви хочете, щоб у стовпчиках нетипових оцінок, було показано символи пів зірок для нецілих оцінок.

re(взірець, замінник) — повертає значення після застосування формального виразу. Усі екземпляри взірця у значенні буде замінено на замінник. У мові шаблонів використано формальні вирази Python [https://docs.python.org/3/library/re.html] без врахування регістру символів.

select(ключ) — обробляє значення як список записів, які відокремлено комами. Кожен запис має форму ідентифікатор:значення (форма identifier calibre). Функція знаходить першу пару із ідентифікатором, який рівний ключеві, і повертає відповідне значення. Якщо відповідних ідентифікаторів не буде виявлено, функція поверне порожній рядок.

shorten(символи ліворуч, текст посередині, символи праворуч) — повертає скорочену версію значення, яка складається із вказаної кількості символи ліворуч символів на початку значення, далі з тексту посередині, і далі кількості символи праворуч символів наприкінці значення. Символи ліворуч і символи праворуч мають бути невід’ємними цілими значеннями. Приклад: припустімо, ви хочете показати заголовок довжиною не більше 15 символів у довжину. Одним шаблоном, який виконує це завдання є {title:shorten(9,-,5)}. Для книги із заголовком Ancient English Laws in the Times of Ivanhoe результатом буде Ancient E-anhoe: перші 9 символів заголовка, -, далі останні 5 символів. Якщо довжина значення є меншою за символи ліворуч + символи праворуч + довжина текст посередині, значення буде повернуто без змін. Наприклад, заголовок The Dome не буде змінено.

str_in_list(роздільник, [ рядок, значення_якщо_знайдено, ]* значення_якщо_не_знайдено) — обробити значення як список записів, які відокремлено символом роздільник, потім порівняти рядок із кожним значенням у списку. Значення рядок не є формальним виразом. Якщо рядок дорівнює будь-якому запису (без врахування регістру символів), повертає відповідне значення_якщо_знайдено. Якщо рядок містить роздільник, його також буде оброблено як список із перевіркою для кожного із вбудованих значень. Можна вказати довільнй кількість пар рядок і значення_якщо_знайдено, що дає змогу повертати різні значення, залежно від значення рядка. Якщо не буде встановлено відповідності жодному з рядків, буде повернуто значення_якщо_не_знайдено. Перевірка рядків виконується у вказаному порядку. Повернуто буде перший з відповідників.

subitems(значення, початковий_номер, кінцевий_номер) — цією функцією користуються для розбиття списків пунктів, зокрема списків жанрів. Функція обробляє значення як відокремлений комами список записів, у якому кожен з записів є відокремленим крапками списком. Повертає новий список, створений видобуванням з кожного запису компонентів від номера початковий_номер до кінцевий_номер. Потім всі видобуті записи знову буде поєднано для отримання результату. Дублікати буде вилучено. Перший компонент у відокремленому крапками списку матиме номер нуль. Якщо номер буде від’ємним, його буде відраховано від кінця списку. Якщо буде вказано кінцевий_номер рівний нулеві, номер вважатиметься кінцевим у списку.

Приклади:

- Припускаючи, що стовпчик #genre містить A.B.C:

- {#genre:subitems(0,1)} повертає «A»

- {#genre:subitems(0,2)} повертає «A.B»

- {#genre:subitems(1,0)} повертає «B.C»

- Припускаючи, що стовпчик #genre містить «A.B.C, D.E»:

- {#genre:subitems(0,1)} повертає «A, D»

- {#genre:subitems(0,2)} повертає «A.B, D.E»

sublist(початковий_номер, кінцевий_номер, роздільник) — обробляє значення як список пунктів, відокремлених роздільником роздільник, повертає новий список, створений з пунктів з номера початковий_номер до номера кінцевий_номер. Перший пункт має нульовий номер. Якщо вказано від’ємний номер, його буде відраховано від кінця списку. Якщо буде вказано кінцевий_номер рівний нулеві, номер вважатиметься кінцевим у списку.

Приклад, у яких припускаємо, що у стовпчику міток (значення відокремлено комами) міститься запис «A, B ,C»:

- {tags:sublist(0,1,\,)} повертає «A»

- {tags:sublist(-1,0,\,)} повертає «C»

- {tags:sublist(0,-1,\,)} повертає «A, B»

swap_around_articles(роздільник) — повертає значення із артиклями, які пересунуто в кінець. Значенням може бути список, елементи якого буде оброблено окремо. Якщо значенням є список, вам слід вказати роздільник значень у списку. Якщо значення роздільник вказано не буде, значення вважатиметься одним рядком, а не списком. Артиклями вважаються рядки, які використовуються calibre для створення title_sort.

swap_around_comma(значення) — за заданими значеннями у форматі B, A повертає значення A B. Найкорисніше для перетворення імен у форматі «Прізвище, Ім’я» на імена у форматі «Ім’я Прізвище». Якщо коми у значенні не буде виявлено, функція поверне значення без змін.

switch([взірець, значення,]+ інше_значення) — для кожної пари ``взірець, значення перевіряє, чи відповідає поле формальному виразу взірець. Якщо це так, повертає «значення». Якщо відповідності з взірцем не буде знайдено, буде повернено «інше_значення». Можна визначати довільну кількість пар взірець, значення. Буде повернуто перший з відповідників.

test(текст для непорожнього, текст для порожнього) — повертає «текст для непорожнього», якщо значення не є порожнім. Якщо це не так, повертає текст для порожнього.

titlecase() — повертає значення поля так, щоб великі літери у результаті використовувалися відповідно до правил щодо заголовків в англійській мові.

transliterate() — повертає рядок латинською абеткою, який сформовано на основі приблизного звучання слів у полі джерела даних. Наприклад, якщо у полі джерела даних міститься запис Фёдор Миха́йлович Достоевский, ця функція поверне Fiodor Mikhailovich Dostoievskii.

uppercase() — повертає значення поля великими літерами.

Використання функцій і форматування у одному шаблоні

Припустімо, у нас є нетиповий стовпчик із назвою #myint, у якому містяться цілі числа. Вам потрібно представити їх у форматі із початковими нулями, ось так: 003. Для цього можна було б скористатися форматуванням 0>3s. Втім, типово, якщо число (ціле або дійсне) дорівнює нулеві, значення буде показано як порожній рядок. Отже, нульові значення початкові значення призводитимуть до порожніх рядків, а не до записів 000. Якщо вам потрібні саме значення 000, вам слід скористатися одразу і рядком форматування, і функцією ifempty для заміни порожнього значення на нульове. Шаблон може бути таким:

{#myint:0>3s:ifempty(0)}

Зауважте, що ви можете також використовувати префікс і суфікс. Якщо ви хочете, щоб число було показано як [003] або [000], скористайтеся таким шаблоном:

{#myint:0>3s:ifempty(0)|[|]}

Загальний режим програмування

Загальний режим програмування замінює `вирази шаблонів програмою, яку написано мовою шаблонів. Синтаксис мови визначено такою граматикою:

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

Нотатки:

- top_expression завжди має значення. Значення expression_list є значенням останнього top_expression у списку. Наприклад, значення списку виразів 1;2;'foobar';3 дорівнює 3.

- У логічному контексті будь-яке непорожнє значення дорівнює True

- У логічному контексті порожнє значення дорівнює False

- Рядки і числа можна використовувати на рівних правах. Наприклад, 10 і '10' є тим самим значенням.

- Коментарями вважаються усі рядки, на початку яких є символ «#». Підтримки коментарів, які починаються не з першого символу рядка, не передбачено.

Пріоритетність операторів

Пріоритетність операторів (порядок обчислення), від найвищої (обчислюються першими) до найнижчої (обчислюються останніми):

- Виклики функцій, сталі, вирази у дужках, вирази інструкцій, вирази надання значення, посилання на поля.

- Унарні плюс (+) і мінус (-). Обчислення для цих операторів виконується справа ліворуч.

Ці і усі інші арифметичні операції повертають цілі числа, якщо обчислення виразу призводить до числа із дробовою частиною, яка рівна нулеві. Приклад: якщо результатом обчислення виразу є 3.0, його буде замінено на 3.

- Множення (*) і ділення (/). Ці дії є асоціативними і обчислюються зліва праворуч. Скористайтеся дужками, якщо ви хочете змінити порядок обчислення.

- Додавання (+) і віднімання (-). Ці дії є асоціативними, обчислення виконується зліва праворуч.

- Порівняння чисел і рядків. Ці оператори повертають '1', якщо порівняння справджується, інакше повертають порожній рядок (''). Порівняння не є асоціативними: a < b < c — синтаксична помилка.

- Поєднання рядків (&). Оператор & повертає рядок, який сформовано об’єднанням виразів ліворуч і праворуч від нього. Приклад: 'aaa' & 'bbb' повертає 'aaabbb'. Оператор є асоціативним і обчислюється зліва праворуч.

- Унарне логічне заперечення (!). Цей оператор повертає '1', якщо вираз не є істинним (є порожнім рядком), інакше повертає ''.

- Логічне «І» (&&). Цей оператор повертає „1“, якщо вирази ліворуч і праворуч від нього є істинними, або порожній рядок '', якщо хоча б одне з тверджень ліворуч і праворуч є хибним. Оператор є асоціативним, обчислюється зліва праворуч, і має властивість скороченого обчислення [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html].

- Логічне «АБО» (||) Цей оператор повертає '1', якщо твердження ліворуч або праворуч від нього є істинним, або '', якщо обидва твердження є хибними. Є асоціативним, обчислюється зліва праворуч і має властивість скороченого обчислення [https://chortle.ccsu.edu/java5/Notes/chap40/ch40_2.html]. Оператор є інклюзивним «АБО», тобто повертає '1', якщо вирази праворуч і ліворуч від нього є істинними.

Посилання на поля

Інструкцію field_reference буде замінено значенням поля метаданих, яке має назву пошуку, яку вказано після $ або $$. Використання $ є еквівалентне до використання функції field(). Використання $$ є еквівалентним до використання функції raw_field. Приклади:

* $authors ==> field('authors')
* $#genre ==> field('#genre')
* $$pubdate ==> raw_field('pubdate')
* $$#my_int ==> raw_field('#my_int')

Вирази if

У виразах if спочатку обчислюється умова. Якщо умова справджується (обчислення дають непорожнє значення), обчислюється expression_list у інструкції then``. Якщо умова не справджується, обчислюється, якщо є, expression_list у інструкції elif або else, якщо існують такі інструкції. Частини elif і else є необов’язковими. Слова if, then, elif, else і fi є зарезервованими, ви не можете використовувати їх як назви ідентифікаторів. Ви можете вставляти розриви рядків та пробіли там, де захочете. Запис condition є top_expression, а не expression_list; не можна використовувати крапку з комою. Набори expression_lists є розділеними крапками з комами послідовності top_expression. Вираз if повертає результат останнього top_expression у обчисленому expression_list або порожній рядок, якщо жодного списку виразів не було обчислено.

Приклади:

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

Приклад вкладеного``if``:

program:
if field('series') then
if check_yes_no(field('#mybool'), '', '', '1') then
'yes'
else
'no'
fi
else
'no series'
fi

Як ми вже згадували вище, if дає значення. Це означає, що усі вказані нижче інструкції є еквівалентними:

* 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

Нарешті, приклад програми, яка повертає значення у стовпчику series, якщо книга належить до циклу, інакше повертає значення стовпчика title:

program: field(if field('series') then 'series' else 'title' fi)

Вирази for

Вираз for виконує ітерацію за списком значень, обробляючи їх по одному за раз. Вираз list_expression має оброблятися до назви для пошуку поля метаданих, наприклад tags або #genre, або списку значень. Функція range() (див. нижче) створює список чисел. Якщо результат є коректною назвою для пошуку, отримується значення поля. Використовується роздільник, вказаний для відповідного типу поля. Якщо результат не є коректною назвою для пошуку, припускається, що це список значень. Припускається, що значення у списку розділено комами, якщо не вказано необов’язкове ключове слово separator. Якщо ключове слово вказано, значення у списку має бути відокремлено результатом обчислення separator_expr. Роздільник не можна використовувати, якщо список створено за допомогою range(). Кожне зі значень у списку надається вказаній змінній, а потім обчислюється expression_list. Ви можете скористатися break для виходу з циклу і continue для переходу на початок циклу для наступної ітерації.

Приклад: цей шаблон вилучає першу ієрархічну назву для кожного значення у полі «Жанр» (#genre), будуючи список із новими назвами:

program:
new_tags = '';
for i in '#genre':
j = re(i, '^.*?\.(.*)$', '\1');
new_tags = list_union(new_tags, j, ',')
rof;
new_tags

Якщо початковим значенням поля «Жанр» було History.Military, Science Fiction.Alternate History, ReadMe, шаблон поверне Military, Alternate History, ReadMe. Ви можете скористатися цим шаблоном в інструменті calibre Пакетне редагування метаданих  →  Пошук із заміною ^ у полі Шукати слід вказати шаблон для вилучення першого рівня ієрархії і визначення значення-результату для поля «Жанр».

Зауваження: останній рядок у шаблоні, new_tags, у цьому випадку не є строго обов’язковим, оскільки for повертає значення останнього top_expression у списку виразів. Значенням виразу надання значення є значення виразу, тому значенням інструкції for є те, що записано до new_tags.

Визначення функції

Якщо у вашому шаблоні є код, який слід виконувати декілька разів, ви можете розмістити цей код у локальній функції. Визначення функції слід розпочинати з ключового слова def. Після нього слід вказати назву функції, список аргументів, а потім код функції. Визначення функції завершується ключовим словом fed.

Аргументи є позиційними. Під час виклику функції надані аргументи заміщують собою зліва праворуч визначені параметри, значення аргументу стає значенням параметра. Надання більшої за кількість визначених параметрів кількості аргументів вважається помилкою. Параметри можуть мати типові значення, наприклад, a = 25. Якщо для параметра не надано аргументу, буде використано типове значення. Якщо такого типового значення немає, буде використано порожній рядок.

У локальній функції можна скористатися інструкцією return.

Перш ніж функцією можна буде скористатися, її слід визначити.

Приклад: цей шаблон обчислює приблизну тривалість періоду часу у роках, місяцях та днях на основі кількості днів. Для форматування обчислених значень використано функцію to_plural(). Зауважте, що у прикладі також використано оператор &:

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

Оператори відношень

Оператори відношень повертають '1', якщо порівняння справджується, інакше повертають порожній рядок („“).

Реалізовано дві форми операторів відношень: порівняння рядків і порівняння чисел.

Порівняння рядків є порівнянням рядків без врахування регістру символів із використанням лексичного порядку. Підтримуваними операторами порівняння рядків є ==, !=, <, <=, >, >=, in та inlist. Для оператора in результат обчислення лівого операнда вважається формальним виразом взірця. Результатом обчислення in є True, якщо взірець у лівого операнда відповідає значенню правого операнда. Значенням оператора inlist є true, якщо формальний вираз ліворуч від нього відповідає будь-якому із записів операнда праворуч від нього; записи слід відокремлювати комами. Відповідність встановлюється без врахування регістру символів.

Операторами числового порівняння є ==#, !=#, <#, <=#, >#, >=#. Результатом обчислення лівого і правого операндів мають бути числові значення з двома виключеннями: рядкове значення «None» (невизначене поле) і порожнє значення вважаються рівними нулеві.

Приклади:

program: field('series') == 'foo' повертає '1', якщо циклом книги є „foo“, інакше повертає ''.

program: 'f.o' in field('series') повертає '1', якщо цикл книги відповідає формальному виразу f.o (наприклад, foo, Off Onyx тощо), інакше повертає ''.

program: 'science' inlist field('#genre') повертає '1', якщо будь-який із записів жанрів книги відповідає формальному виразу science, наприклад, Science, History of Science, Science Fiction тощо). У інших випадках буде повернуто ''.

program: '^science$' inlist field('#genre') повертає '1', якщо будь-який із записів жанрів книги точно відповідає формальному виразу ^science$, наприклад, Science. Жанри History of Science і Science Fiction вважаються невідповідними. Якщо відповідність не буде встановлено, програма поверне ''.

program: if field('series') != 'foo' then 'bar' else 'mumble' fi повертає 'bar', якщо циклом книги не є foo, інакше повертає mumble.

program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi повертає yes, якщо циклом є 'foo' або '1632', інакше повертає no.

program: if '^(foo|'1632)$' in field('series') then 'yes' else 'no' fi повертає 'yes', якщо циклом є 'foo' або '1632', інакше повертає 'no'.

program: if 11 > 2 then 'yes' else 'no' fi повертає 'no', оскільки оператор > виконує лексичне порівняння.

program: if 11 > 2 then 'yes' else 'no' fi повертає 'yes', оскільки оператор ># виконує числове порівняння.

Додаткові функції

Окрім функцій, які описано у розділі Режим єдиної функції, можна скористатися описаними нижче функціями.

У загальному режимі програмування функції, які описано для режиму єдиної функції, усі потребують додаткового першого параметра, який визначає значення, яке слід обробляти. Усі параметри належать до expression_lists (див. граматику вище).

add(x [, y]*) — повертає суму своїх аргументів. Видає виключення, якщо аргумент не є числом. У більшості випадків ви можете скористатися замість цієї функції оператором +.

and(значення [, значення]*) — повертає рядок «1», якщо усі значення не є порожніми, інакше повертає порожній рядок. Кількість значень може бути довільною. У більшості випадків замість цієї функції можна скористатися оператором &&. Однією з причин не замінювати and на && є випадки, коли оптимізація обчислення може змінити результати через сторонні ефекти. Наприклад, and(a='',b=5) виконує обидва записи значень, а оператор && не виконає другого з них.

assign(ідентифікатор, значення) — надає ідентифікатору значення, потім повертає значення. ідентифікатор має бути ідентифікатором, а не виразом. У більшості випадків можна замість цієї функції використовувати оператор =.

approximate_formats() — повернути розділений комами список форматів, які пов’язано з книгою. Гарантувати чинність такого списку не можна, хоча, ймовірно, він залишається чинним. Цю функцію та інші функції без параметрів можна викликати у режимі шаблонного програмування (див. нижче) за допомогою шаблона «{:“approximate_formats()“}». Зауважте, що назви форматів завжди слід вказувати великими літерами, ось так: EPUB. Функція approximate_formats() є значно швидшою за функції formats_..., які описано нижче.

author_links(роздільник_значень, роздільник_пар) — повертає рядок, що містить список авторів з відповідними пов’язаними значеннями у такому форматі:

author1 val_separator author1_link pair_separator author2 val_separator author2_link etc.

Запис автора буде відділено від пов’язаного із ним значення рядком роздільник_значень без додаткових пробілів. Пари автор:пов'язане значення буде відокремлено рядком роздільник_пар без додаткових пробілів. Ви можете вибирати рядки роздільників довільним чином, так, щоб вони не збігалися з іменами авторів або посиланнями. Запис автора буде включено, навіть якщо пов’язане з ним значення посилання є порожнім.

author_sorts(роздільник_значень) — повертає рядок, що містить список значень упорядкування записів авторів книги. Критерієм упорядковування є один із записів метаданих автора (відмінний від author_sort у книгах). Повернутий список має такий формат: упорядкування автора 1 роздільник_значень упорядкування автора 2 тощо без додавання пробілів. Значення упорядковування авторів у цьому списку перебувають у тому самому порядку, що і автори у книзі. Якщо вам потрібні пробіли навколо роздільника_значень, додайте їх до рядка роздільника.

book_count(запит, використати_віртуальні) — повертає кількість книг, які було знайдено за запитом запит. Якщо значенням використати_віртуальні є 0 (нуль), віртуальні бібліотеки буде проігноровано. Ця функція і подібна до неї функція book_values() корисні, зокрема, у шаблонних пошуках, де передбачено підтримку пошуків, які поєднують відомості з багатьох книг, наприклад, пошуків циклів, де є лише одна книга. Функцією не можна користуватися у складених стовпчиках, якщо для коригування allow_template_database_functions_in_composites не встановлено значення True. Можна використовувати лише у графічному інтерфейсі.

Наприклад, у цьому шаблонному пошуку використано цю функцію і подібну до неї функцію для пошуку усіх циклів, які містять лише одну книгу:

1. Визначення збереженого шаблона (за допомогою пункту Налаштування → Додатково → Шаблонні функції) із назвою series_only_one_book (назва може бути довільною). Шаблон:

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, '')

Під час першого запуску шаблона (перевірка першої книги) він зберігає результати пошуків у базі даних у загальній шаблонній змінній із назвою vals. Ці результати буде використано для подальшого пошуку книг без повторної обробки усієї бази даних.

2. Використання збереженого шаблона у шаблонному пошуку:

template:"program: series_only_one_book()#@#:n:1"

Використання збереженого шаблона замість вписування шаблона у пошук усуває проблеми, які пов’язано із вимогою щодо екранування лапок у виразах для пошуку.

book_values(стовпчик, запит, роздільник, використати_віртуальні) — повертає список неповторюваних значень, що містяться у стовпчику стовпчик (назві пошуку), відокремлені роздільником роздільник, у книгах, які було знайдено за запитом запит. Якщо значенням використати_віртуальні є 0 (нуль), віртуальні бібліотеки буде проігноровано. Ця функція і подібна до неї функція book_count() корисні, зокрема, у шаблонних пошуках, де передбачено підтримку пошуків, які поєднують відомості з багатьох книг, наприклад, пошуків циклів, де є лише одна книга. Функцією не можна користуватися у складених стовпчиках, якщо для коригування allow_template_database_functions_in_composites не встановлено значення True. Можна використовувати лише у графічному інтерфейсі.

booksize() — повертає значення поля calibre «size». Повертає „“, якщо форматів немає.

check_yes_no(назва_поля, is_undefined, is_false, is_true) — перевіряє, чи дорівнює значення поля «так/ні», вказаного назвою для пошуку назва_поля, значенню, вказаному параметрами, повертає 'yes', якщо знайдено відповідник, інакше, повертає порожній рядок. Встановіть для параметрів is_undefined, is_false або is_true значення 1 (число), щоб перевірити відповідну умову, інакше, встановіть 0. Приклад:

check_yes_no("#bool", 1, 0, 1) повертає 'yes', якщо "#bool" поля так/ні або не визначено (він не є істинним і не є хибним), або дорівнює True.

Значення 1 можна встановлювати для декількох записів з набору is_undefined, is_false або is_true.

ceiling(x) — повертає найменше ціле число, яке є більшим або рівним x. Надсилає виключення, якщо x не є числом.

character(назва_символу) — повертає символ із назвою «назва_символу». Наприклад, character('newline') повертає символ розриву рядка ('\n'). Передбачено підтримку таки назв символів: newline, return, tab і backslash.

cmp(x, y, lt, eq, gt) — порівнює x і y після перетворення обох величин у числа. Повертає lt, якщо x <# y`, eq, якщо x ==# y, у всіх інших випадках повертає gt. Цю функцію зазвичай можна замінити одним із операторів порівняння чисел (==#, <#, ># тощо).

connected_device_name(ключ_розташування_сховища) — якщо пристрій з’єднано, повертає назву пристрою, якщо ні — порожній рядок. Кожне розташування сховища даних на пристрої має власну назву. Значення параметра ключ_розташування_сховища: 'main', 'card' і 'cardb'. Ця функція працює лише у графічному інтерфейсі користувача.

connected_device_uuid(ключ_розташування_сховища) — якщо пристрій з’єднано, повертає UUID (унікальний ідентифікатор) пристрою, якщо ні — порожній рядок. Кожне розташування сховища даних на пристрої має власний UUID. Значення параметра ключ_розташування_сховища: 'main', 'carda' і 'cardb'. Ця функція працює лише у графічному інтерфейсі користувача.

current_library_name() — повертає останню адресу поточної бібліотеки calibre.

current_library_path() — повертає повний шлях до поточної бібліотеки calibre.

current_virtual_library_name() — повертає назву поточної віртуальної бібліотеки, якщо така є, і порожній рядок, якщо такої немає. Регістр символів назви бібліотеки буде збережено. Приклад: program: current_virtual_library_name(). Ця функція працює лише у режимі графічного інтерфейсу користувача.

date_arithmetic(дата, специфікація_обчислень, формат) — обчислення нової дати на основі дати дата з використанням специфікації_обчислень. Повертає нову дату, форматовану відповідно до додаткового параметра формат. Якщо формат не вказано, результат буде виведено у форматі ISO. Параметр специфікація_обчислень є рядком, який сформовано поєднанням пар зО (значенняОперація), де з є можливо від’ємним числом, а О є однією з таких літер:

- s — додати з секунд до значення дата

- m — додати з хвилин до значення дата

- h — додати з годин до значення дата

- d — додати з днів до значення дата

- w — додати з тижнів до значення дата

- y — додати з років до значення дата, де роком вважаються 365 днів.

Приклад: 1s3d-1m — додати 1 секунду, додати 3 дні і відняти 1 хвилину від дати дата.

days_between(дата1, дата2) — повертає кількість днів між датою дата1 і датою дата2``. Кількість днів є додатною, якщо дата1 перевищує дата2, інакше значення буде від’ємним. Якщо значення дата1 або значення дата2 не вдасться обробити як дату, функція поверне порожній рядок.

divide(x, y) — повертає частку x / y. Повідомляє про виключення, якщо x або y не є числом. Зазвичай, цю функцію можна замінити оператором /.

``eval(рядок) — обробляє рядок як програму з передаванням локальних змінних <template_mode>`. Це надає змогу скористатися обробником шаблонів для побудови складних результатів на основі локальних змінних. У режимі програмування шаблонів символи «{» і «}» обробляють до обробки шаблона, тому вам слід використовувати «[[» для позначення символу «{» і «]]» для позначення символу «}». Перетворення буде виконано автоматично. Також слід зауважити, що у аргументі цієї функції не можна використовувати префікси і суфікси (тобто синтаксичні конструкції, подібні до «|префікс|суфікс»), якщо використовується режим програмування шаблонів.

extra_file_size(назва_файла) — повертає розмір у байтах додаткового файла «назва_файла» у теці «data/» книги, якщо він існує. Якщо файла не існує, повертає -1. Див. також функції has_extra_files(), extra_file_names() та extra_file_modtime(). Цією функцією можна скористатися лише у графічному інтерфейсі.

extra_file_modtime(назва_файла, рядок_формату) — повертає час внесення змін до додаткового файла назва_файла у теці data/ книги, якщо він існує. Якщо файла не існує, повертає -1. Час внесення змін буде форматовано відповідно до рядка формату (див. format_date()). Якщо специфікатор формату є порожнім, повертає час внесення змін як десятковий дріб секунд з початку епохи. Див. також функції has_extra_files(), extra_file_names() та extra_file_size(). Значення епохи є специфічним для операційної системи. Цією функцією можна скористатися лише у графічному інтерфейсі.

extra_file_names(роздільник[, взірець]) — список відокремлених роздільником назв файлів у теці data/ книги. Якщо вказано необов’язковий параметр «взірець» (формальний вираз), список буде відфільтровано до файлів, які відповідають взірцю. Відповідність визначатиметься без врахування регістру символів. Див. також функції has_extra_files(), extra_file_modtime() та extra_file_size(). Цією функцією можна скористатися лише у графічному інтерфейсі.

field(назва_для_пошуку) — повертає значення поля метаданих із назвою для пошуку назва_для_пошуку.

field_exists(назва_поля) — перевіряє, чи існує поле (стовпчик) із назвою для пошуку назва_поля. Повертає '1', якщо поле існує, і порожній рядок, якщо не існує.

finish_formatting(значення, формат, префікс, суфікс) — застосувати формат, префікс і суфікс до значення у спосіб, у який це виконується шаблоном, зокрема {series_index:05.2f| - |- }. Цю функцію створено для спрощення перетворення складних шаблонів режиму єдиної функції і програмного режиму до шаблонів загального програмного режиму. Наприклад, наведена нижче програма виводить ті самі дані, що і вказаний вище програма-шаблон:

program: finish_formatting(field("series_index"), "05.2f", " - ", " - ")

Інший приклад. Для шаблона {series:re(([^\s])[^\s]+(\s|$),\1)}{series_index:0>2s| - | - }{title} скористайтеся таким:

program:
strcat(
re(field('series'), '([^\s])[^\s]+(\s|$)', '\1'),
finish_formatting(field('series_index'), '0>2s', ' - ', ' - '),
field('title')
)

first_matching_cmp(значення, [ умова, результат, ]* інший_результат) — послідовно виконує порівняння значення < умова, повертаючи пов’язаний результат для першої з виконаних умов у послідовності. Повертає інший_результат, якщо жодну з умов не буде виконано. Приклад:

i = 10;
first_matching_cmp(i,5,"small",10,"middle",15,"large","giant")

повертає "large". Той самий приклад із першим значенням 16 повертатиме "giant".

first_non_empty(значення [, значення]*) — повертає перше значення, яке не є порожнім. Якщо всі значення є порожніми, повертає порожній рядок. Ви можете вказати довільну кількість значень.

floor(x) — повертає найбільше ціле число, яке є меншим або рівним x. Надсилає виключення, якщо x не є числом.

format_date(значення, рядок_формату) — форматувати значення, яке має бути рядком дати, на основі значення рядок_формату, повернути рядок. Коди форматування:

- d    : номер дня без початкового нуля (від 1 до 31)

- dd   : номер дня з початковим нулем (від 01 до 31)

- ddd  : скорочена локалізована назва дня тижня (від «пн» до «нд»).

- dddd : довга локалізована назва дня (від «понеділок» до «неділя»).

- M    : номер місяця без початкового нуля (від 1 до 12).

- MM   : номер місяця з початковим нулем (від 01 до 12)

- MMM  : скорочена локалізована назва місяця (від «січ» до «гру»).

- MMMM : повна локалізована назва місяця (від «січень» до «грудень»).

- yy   : рік як двоцифрове значення (від 00 до 99).

- yyyy : рік як чотирицифрове значення.

- h    : години без початкового 0 (від 0 до 11 або від 0 до 23, залежно від am/pm)

- hh   : години з початковим 0 (від 00 до 11 або від 00 до 23, залежно від am/pm)

- m    : хвилини без початкового 0 (від 0 до 59)

- mm   : хвилини з початковим 0 (від 00 до 59)

- s    : секунди без початкового 0 (від 0 до 59)

- ss   : секунди з початковим 0 (від 00 до 59)

- ap   : використовувати 12-годинний формат замість 24-годинного, із „ap“, яке буде замінено на локалізований рядок для am або pm.

- AP   : використовувати 12-годинний формат замість 24-годинного, із „AP“, яке буде замінено на локалізований рядок для am або pm.

- iso  : дата із часом і часовим поясом. Має бути єдиним вказаним значенням форматування.

- to_number   : перетворити дату і час у число із рухомою крапкою (часову позначку)

- from_number : перетворити число з рухомою крапкою (часову позначку) на дату у форматі iso. Якщо вам потрібен інший формат дати, додайте бажаний рядок форматування після from_number і двокрапки (:). Приклад: from_number:MMM dd yyyy

Результати можуть бути не такими, на які ви сподівалися, якщо форматована дата містить локалізовані назви місяців, що може трапитися, якщо ви змінили коригування формату на такі, які містять MMMM. У цьому випадку скористайтеся функцією field(), як у прикладі:

format_date(field('pubdate'), 'yyyy')

скористайтеся функцією raw_field(), як у:

format_date(raw_field('pubdate'), 'yyyy')

format_date_field(назва_поля, рядок_форматування) — форматувати значення у полі назва_поля, яке має бути фільтром поля дати, стандартного або нетипового. Див. format_date(), щоб дізнатися більше про коди форматування. Ця функція є набагато швидшою за format_date. Нею слід користуватися, якщо ви форматуєте значення у полі (стовпчику). Нею не можна користуватися для обчислених дат або дат у рядкових змінних. Приклади:

format_date_field('pubdate', 'yyyy.MM.dd')
format_date_field('#date_read', 'MMM dd, yyyy')

formats_modtimes(рядок_формату_дати) — повертає відокремлений комами список відокремлених двокрапками пунктів ФОРМАТ:ДАТА, які відповідають часу внесення змін до файлів форматів книги. Параметр рядок_формату_дати визначає спосіб форматування дати. Докладніше про цей формат можна дізнатися з опису функції format_date(). Для отримання даних щодо часу внесення змін до одного з форматів ви можете скористатися функцією select. Зауважте, що назви форматів буде вказано великими літерами, наприклад EPUB.

formats_paths() — повернути розділений комами список розділених двокрапками записів ФОРМАТ:ШЛЯХ, щоб відповідають шляхам до файлів книги у різних форматах. Для отримання шляху до файла певного формату ви можете скористатися функцією select. Зауважте, що назви форматів слід вказувати великими літерами, ось так: EPUB.

formats_sizes() — повертає відокремлений комами список відокремлених двокрапками пунктів ФОРМАТ:РОЗМІР, які відповідають об’єму у байтах файлів форматів книги. Для отримання даних щодо об’єму файла у одному з форматів ви можете скористатися функцією select. Зауважте, що назви форматів буде вказано великими літерами, наприклад EPUB.

fractional_part(x) — повертає дробову частину десяткового числа. Наприклад, fractional_part(3.14) дорівнює 0.14. Надсилає виключення, якщо x не є числом.

get_link(назва_поля, значення_поля) — отримати посилання для поля назва_поля зі значенням значення_поля. Якщо немає пов’язаного посилання, повертає порожній рядок. Приклади:

- Наведена нижче команда повертає посилання, яке пов’язане із міткою Fiction:

get_link('tags', 'Fiction')

- Цей шаблон створює список посилань для усіх міток, які пов’язано із книгою, у форматі значення:посилання, ...:

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

hascover() — повертає «Yes», якщо у книги є зображення обкладинки, інакше повертає порожній рядок.

has_extra_files([взірець]) — повертає кількість додаткових файлів, якщо такі є. Якщо файлів немає, повертає „“ (порожній рядок). Якщо вказано необов’язковий параметр «взірець» (формальний вираз), список, перш ніж буде виконано обчислення кількості, буде відфільтровано до файлів, які відповідають взірцю. Відповідність визначатиметься без врахування регістру символів. Див. також функції extra_file_names(), extra_file_size() та extra_file_modtime(). Цією функцією можна скористатися лише у графічному інтерфейсі.

identifier_in_list(значення, назва_ідентифікатора [, значення_якщо_знайдено, значення_якщо_не_знайдено]) — обробити значення як список відокремлених комами ідентифікаторів. Ідентифікатор слід записувати у форматі «назва_ідентифікатора:значення». Параметр назва_ідентифікатора є текстом назви ідентифікатора, який слід знайти, або у форматі «назва_ідентифікатора» або «назва_ідентифікатора:формальний_вираз». Відповідність у першому випадку буде встановлено, якщо буде виявлено будь-який ідентифікатор, що відповідає критерію «назва_ідентифікатора». У другому випадку відповідність буде встановлено, якщо «назва_ідентифікатора» відповідає ідентифікатору, а «формальний_вираз» відповідає значенню ідентифікатора. Якщо задано значення_якщо_знайдено і значення_якщо_не_знайдено, якщо буде встановлено відповідність, програма поверне значення_якщо_знайдено. Якщо нічого не буде знайдено, програма поверне значення_якщо_не_знайдено. Якщо не задано значення_якщо_знайдено і значення_якщо_не_знайдено, якщо буде встановлено відповідність, програма поверне пару ідентифікатор:значення. Якщо значення не задано, буде повернуто порожній рядок ('').

is_marked() — перевірити, чи має книга стан marked у calibre. Якщо це так, буде повернуто значення позначки, або 'true' (у нижньому регістрі), або список іменованих позначок, які відокремлено комами. Повертає '' (порожній рядок), якщо книгу не позначено. Ця функція працює лише у графічному інтерфейсі.

language_codes(рядки_мов) — повертає коди мов [https://www.loc.gov/standards/iso639-2/php/code_list.php] для назв мов, які передано за допомогою аргументу рядки_мов. Рядки має бути вказано мовою поточної локалі. Аргумент рядки_мов є списком значень, які відокремлено комами.

list_contains(значення, роздільник, [ взірець, значення_якщо_знайдено, ] значення_якщо_не_знайдено) — (альтернатива in_list) обробляє значення як список пунктів, відокремлених роздільником, порівнює взірець з кожним зі значень у списку. Якщо рядок збігається зі значенням, повертає значення_якщо_знайдено, інакше повертає значення_якщо_не_знайдено. Значення взірець і значення_якщо_знайдено можна повторювати довільну кількість разів, залежно від пошуку. Перевірка взірців виконується послідовно. Функція повертає перший знайдений відповідник. Варіанти: in_list(), list_contains()

list_count(значення, роздільник) — за заданим значенням списком пунктів, відокремлених роздільником роздільник, повертає кількість пунктів у списку. Варіанти: count(), list_count()

list_count_matching(список, взірець, роздільник) — за заданим значенням списком пунктів, відокремлених роздільником «роздільник», повертає кількість пунктів у списку, які відповідають заданому формальним виразом взірцю. Варіанти: list_count_matching(), count_matching()

list_difference(список1, список2, роздільник) — повертає список, створений вилученням зі списку список1 всіх пунктів, які містяться у списку список2, на основі порівняння без врахування регістру. Пункти у списках список1 і список2 відокремлюються роздільником «роздільник», як і пункти у повернутому функцією списку.

list_equals(список1, роздільник1, список2, роздільник2, значення_якщо_так, значення_якщо_ні) — повертає значення_якщо_так, якщо список1 і список2 містять однакові записи``. Якщо це не так, повертає значення_якщо_ні. Записи визначаються поділом кожного зі списків на основі вказаного символу роздільника (роздільник1 або роздільник2). Порядок записів у списках не береться до уваги. Порівняння виконуватиметься без врахування регістру символів.

list_intersection(список1, список2, роздільник) — повертає список, створений вилученням зі списку список1 всіх пунктів, яких немає у списку список2, на основі порівняння без врахування регістру. Пункти у списках список1 і список2 відокремлюються роздільником «роздільник», як і пункти у повернутому функцією списку.

list_join(із_роздільником, список1, роздільник1 [, список2, роздільник2]*) — повертає список, створений об’єднанням записів у початкових списках (список1, список2 тощо) з використанням роздільника із_роздільником між записами у списку-результаті. Записи у кожному із початкових списків список[123...] розділено відповідним роздільником роздільник[123...]. У списку можуть міститися нульові значення. Він може бути полем, подібним до поля видавця, яке має одне значення, фактично, списком з одного запису. Дублікати буде вилучено з використанням порівняння без врахування регістру символів. Записи буде повернуто у порядку, у якому їх вказано у початкових списках. Якщо записи у списках відрізняються лише регістром символів, буде використано останній з них. Усі роздільники можуть складатися з декількох символів.

Приклад:

program:
list_join('#@#', $authors, '&', $tags, ',')

Ви можете скористатися list_join для обробки результатів попередніх викликів list_join ось так:

program:
a = list_join('#@#', $authors, '&', $tags, ',');
b = list_join('#@#', a, '#@#', $#genre, ',', $#people, '&', 'some value', ',')

Для створення списку ви можете скористатися виразами. Наприклад, припустімо, вам потрібні записи для authors і #genre, але із жанром, заміненим на слово «Genre: », за яким має бути вказано першу літеру жанру, тобто, якщо жанром є «Fiction», матимемо «Genre: F». Результату можна досягти так:

program:
list_join('#@#', $authors, '&', list_re($#genre, ',', '^(.).*$', 'Genre: \1'), ',')

list_re(початковий_список, роздільник, фв_пошуку, пар_заміни) — будує список таким чином: спочатку поділяє початковий_список на запис на основі вказаного символу роздільник. Для кожного з записів у списку перевіряє відповідність формальному виразу фв_пошуку. Якщо відповідність буде встановлено, запис буде додано до повернутого списку. Якщо параметр пар_заміни не буде порожнім, заміну буде виконано до додавання запису до повернутого списку.

list_re_group(список_джерел, роздільник, фв_включення, фв_пошуку [, шаблон_групи]*) — Подібне до list_re, але замінники є обов’язковими. Використовує re_group(пункт_списку, фв_пошуку, шаблон ...) під час виконання замін у остаточному списку.

list_remove_duplicatesunion(список, роздільник) — повертає список, створений вилученням дублікатів з початкового списку. Якщо записи пунктів відрізнятимуться лише регістром символів, буде повернуто лише останній з них. Пункти у списку має бути відокремлено роздільником, який також буде використано під час формування списку-результату.

list_sort(список, напрямок, роздільник) — повертає список, впорядкований без врахування регістру. Якщо значенням параметра напрямок є нуль, список буде впорядковано за зростанням. Якщо значення буде іншим, список впорядковуватиметься за спаданням. Записи у початковому списку має бути відокремлено роздільником, як і записи у списку-результаті.

lis_split(значення_список, роздільник, префікс_ідентифікатора) — ділить значення_список на окремі значення з використанням роздільника роздільник, а потім надає значення локальним змінним із назвами префікс_ідентифікатора_N, де N — позиція значення у списку. Перший запис має позицію 0 (нуль). Функція повертає останній елемент у списку.

Приклад:

list_split('one:two:foo', ':', 'var')

дає такий результат:

var_0 = 'one';
var_1 = 'two';
var_2 = 'foo

list_union(список1, список2, роздільник) — повертає список, створений за допомогою об’єднання пунктів у списках список1 і список2` з вилученням дублікатів, виявлених шляхом порівняння без врахування регістру символів. Якщо записи пунктів відрізнятимуться лише регістром символів, використовуватиметься пункт зі списку ``список1. Пункти у списках список1 і список2 має бути відокремлено роздільником, який також буде використано під час формування списку-результату. Варіанти: merge_lists()`, list_union()

mod(x) — повертає floor від лишку від ділення x / y. Повідомляє про виключення, якщо x або y не є числом.

multiply(x [, y]*) — повертає добуток аргументів. Надсилає повідомлення про виключення, якщо аргумент не є числом. Зазвичай, цю функцію можна замінити оператором *.

not(значення) — повертає рядок «1», якщо значення не є порожнім. Якщо значення є порожнім, повертає порожній рядок. Зазвичай, цю функцію можна замінити на унарний оператор «НІ» (!).

ondevice() — повертає 'Yes', якщо встановлено ondevice, інакше повертає порожній рядок

or(значення [, значення]*) — повертає рядок "'1", якщо хоч одне значення не є порожнім. Якщо всі значення є порожніми, повертає порожній рядок. Кількість значень може бути довільною. Зазвичай, цю функцію можна замінити оператором ||. Причиною того, що її не можна замінити, є те, що при оптимізації обчислення може бути змінено результати через сторонні ефекти.

print(a [, b]*) — виводить аргументи до стандартного виведення. Якщо ви не запускали calibre з командного рядка (calibre-debug -g), дані нікуди не буде виведено. Функція print завжди повертає свій перший аргумент.

range(початок, кінець, крок, обмеження) — повертає список, який створено циклічним проходженням діапазону, який вказано параметрами «початок», «кінець» та «крок» із максимальною довжиною «обмеження». Першим значенням у списку буде «початок». Наступними значеннями будуть наступне_значення = поточне_значення + крок. Проходження циклу триватиме, доки наступне_значення < кінець , якщо крок є додатним, інакше, доки наступне_значення > кінець. Буде виведено порожній список, якщо не буде виконано умову початок >= кінець і крок є додатним. Значення обмеження встановлює максимальну довжину списку. Типовим значенням є 1000. Параметри початок, крок і обмеження є необов’язковими. Виклик range() із одним аргументом задає кінець. Два аргументи задають початок і кінець. Три аргументи задають початок, кінець і крок. Чотири аргументи задають початок, кінець, крок і обмеження. Приклади:

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(назва_для_пошуку [, необов'язкове_типове_значення]) — повертає вміст іменованого поля метаданих без застосування форматування. Обробляє і повертає додатковий другий аргумент, необов'язкове_типове_значення, якщо значення поля не визначено (None).

raw_list(назва_для_пошуку, роздільник) — повертає список метаданих з назвою «назва_для_пошуку» без застосування будь-якого форматування або упорядковування. Записи у списку буде відокремлено значенням «роздільник».

re_group(значення, взірець [, шаблон_для_групи]*) — повертає рядок, створений за допомогою застосування формального виразу заданого зразком до значення та заміною кожного з відповідників значенням, повернутим відповідним шаблоном. У режимі шаблонного програмування, як і у функціях template та eval, замість { слід використовувати [[, замість } — ]].

У наведеному нижче прикладі виконується пошук записів циклів із декількох слів і перша літера першого слова замінюється на велику:

program: re_group(field('series'), "(\S* )(.*)", "{$:uppercase()}", "{$}")'}

round(x) — повертає найближче до x ціле число. Надсилає виключення, якщо x не є числом.

series_sort() — повертає значення впорядкування циклу.

strcat(a [, b]*) — може приймати довільну кількість аргументів. Повертає рядок, отриманий з’єднанням всіх аргументів..

strcat_max(максимум, рядок1 [, префікс2, рядок2]*) — повертає рядок, утворений об’єднанням аргументів функції. Початковим значенням результату буде рядок1. Пари префікс, рядок додаватимуться в кінець результату, якщо довжина рядка-результату не перевищуватиме значення максимум. Префікси можуть бути порожніми. Значення рядок1 буде повернуто, навіть якщо довжина рядка рядок1 перевищує максимум. Функції можна передавати довільну кількість пар префікс, рядок.

strcmp(x, y, lt, eq, gt) — виконує лексичне порівняння x і y без врахування регістру. Повертає lt, якщо x < y`, ``eq, якщо x == y, в усіх інших випадках повертає gt. Цю функцію часто може бути замінено одним із операторів лексичного порівняння (==, >, < тощо)

strcmpcase(x, y, lt, eq, gt) — виконує лексичне порівняння x і y із врахуванням регістру. Повертає lt, якщо x < y. Повертає eq, якщо x == y. За інших умов повертає gt.

Зауваження: це НЕ типова поведінка calibre, наприклад, у операторах лексичного порівняння (==, >, < тощо). Використання цієї функції може призвести до неочікуваних результатів, радимо вам користуватися, якщо це можливо, strcmp().

strlen(значення) — повертає довжину рядка значення.

substr(рядок, початок, кінець) — повертає символи від символу з номером початок до символу з номером кінець у рядку рядок. Вважається, що у рядок починається з нульового символу. Якщо значення кінець є від’ємним, символи буде відраховано у напрямку до початку рядка. Якщо значенням кінець є нуль, кінцем вважатиметься останній символ. Приклади: substr('12345', 1, 0) повертає '2345', а substr('12345', 1, -1) повертає '234'.

subtract(x, y) — повертає різницю x - y. Повідомляє про виключення, якщо x або y не є числом. Зазвичай, цю функцію можна замінити оператором -.

switch_if([вираз_перевірки, вираз_значення,]+ альтернативний_вираз) — для кожної пари вираз_перевірки, вираз_значення перевіряє, чи дорівнює вираз_перевірки True (непорожнє значення) і, якщо це так, повертає результат вираз_значення. Якщо, жоден вираз_перевірки не дорівнює True, буде повернуто альтернативний_вираз. Можлива кількість пар вираз_перевірки, вираз_значення є необмеженою.

today() — повертає сьогоднішню дату і час (зараз). Це значення створено для використання у format_date або days_between, але з ним можна працювати як зі звичайним рядком. Дату буде повернуто у форматі ISO <https://en.wikipedia.org/wiki/ISO_8601>`_.

template(x) — обробляє x як шаблон. Обробка виконується у власному контексті, тобто змінні не є спільними для обробки функції виклику і шаблона.

to_hex(значення) — повертає рядок значення, який закодовано шістнадцятковими числами. Корисно при побудові адрес calibre.

urls_from_identifiers(ідентифікатори, упорядкувати_результати) — за заданим списком відокремлених комами ідентифікаторів, де ідентифікатором є пара відокремлених двокрапкою значень (назва:значення_ідентифікатора), повертає список відокремлених комами адрес HTML на основі ідентифікаторів. Список лишиться неупорядкованим, якщо упорядкувати_результати має значення 0 (символ або число), інакше, список буде упорядковано за назвами ідентифікаторів. Адреси буде створено у той самий спосіб, що і для вбудованого стовпчика ідентифікаторів, який показано на панелі подробиць щодо книги.

Складніші програми у виразах шаблонів — режим програмування шаблонів.

Режим програмування шаблонів є сумішшю загального режиму програмування і режиму єдиної функції. Режим програмування шаблонів відрізняється від режиму єдиної функції тим, що у ньому можна писати вирази шаблонів, які посилаються на інші поля метаданих, використовувати вкладені функції, змінювати значення змінних і виконувати арифметичні дії. Він відрізняється від загального режиму програмування тим, що шаблон міститься між символами { і } і не починається зі слова program:. Програмна частина шаблона є списком виразів загального режиму програмування.

Почнімо з прикладу. Припустімо, що ви хочете, щоб ваш шаблон показував цикл для книги, яка належить до циклу, і значення нетипового поля #genre, якщо книга не належить до циклів. Зробити це за допомогою режиму єдиної функції неможливо, оскільки ви не можете послатися на інше поле метаданих у виразі шаблона. Але у програмному режимі ви можете це зробити. Спрацює такий вираз:

{#series:'ifempty($, field('#genre'))'}

Приклад демонструє декілька речей:

- Режим програмування шаблонів буде використано, якщо вираз починається з :' і завершується '}. Усі інші варіанти вважатимуться використанням режиму єдиної функції.

- змінна $ позначає іменоване у шаблоні поле, яке обробляє вираз; у нашому випадку це #series.

- функціями мають передаватися усі їхні аргументи. Типового значення немає. Наприклад, стандартним вбудованим функціями слід передавати додатковий початковий параметр, який вказує на поле джерела даних.

- пробіли буде проігноровано; їх можна використовувати будь-де у виразі.

- сталі рядки слід брати у відповідні лапки ' або ".

У режимі програмування шаблонів можна користуватися усіма функціями зі списків режиму єдиної функції і загальному режимі програмування.

У режимі програмування шаблонів використання символів { і } у рядкових літералах режиму програмування шаблонів може призвести до помилок або неочікуваних результатів, оскільки ці символи заважають нормальній роботі обробника шаблонів. Він намагається обробити їх як межі виразу шаблона, а не символи. У деяких, але не в усіх випадках, ви можете замінити { на [[ і } на ]]. Загалом кажучи, якщо у вашій програмі є символи { і }, вам слід користуватися загальним режимом програмування.

Як і у загальному режимі програмування, для функцій з документації до режиму єдиної функції вам доведеться передати функції значення, з яким вона має працювати, як перший параметр, додаткового до документованих параметрів. У режимі програмування шаблонів ви можете скористатися $ для доступу до значення, яке вказано назвою для пошуку для виразу шаблона.

Режим шаблонів Python

Режим шаблонів Python (PTM) надає вам змогу писати шаблони за допомогою мови Python та програмного інтерфейсу calibre [https://manual.calibre-ebook.com/develop.html#api-documentation-for-various-parts-of-calibre]. Найкориснішим програмним інтерфейсом буде програмний інтерфейс роботи з базою даних. Подальше обговорення не належить до предмета цього підручника. Шаблони PTM є швидшими і можуть виконувати складніші дії, але вам потрібні будуть значення щодо написання коду мовою Python за допомогою програмного інтерфейсу calibre.

Шаблон PTM розпочинається так:

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'

Ви можете додати наведений вище текст до вашого шаблона за допомогою контекстного меню, доступ до якого можна отримати за допомогою клацання правою кнопкою миші. Коментарі є необов’язковими — їх можна вилучити. Вам слід використовувати відступи, які використовують у Python.

Для об’єкта контексту передбачено підтримку str(контекст), яка повертає рядок вмісту контексту, та context.attributes, який повертає список назв атрибутів у контексті.

За допомогою атрибуту context.funcs можна викликати вбудовані функції та функції шаблонів користувача, а також збережені шаблони GPM/Python так, щоб їх можна було виконати безпосередньо у вашому коді. Функції буде отримано за назвами. Якщо виникне конфлікт назв із ключовим словом Python, додайте символ підкреслювання наприкінці назви. Приклади:

context.funcs.list_re_group()
context.funcs.assert_()

Нижче наведено приклад шаблона PTM, який створює список усіх авторів для циклу книг. Список буде збережено до «стовпчика побудованого на основі інших стовпчиків, який поводитиме себе як мітки». Його буде показано у Подробицях щодо книги, для нього буде використано позначений стан пункту в окремих рядках (на сторінці Налаштування → Вигляд та поведінка → Подробиці щодо книги). Використання цього пункту потребуватиме відокремлення записів у списку комами. Щоб виконати цю вимогу, шаблон перетворить коми у іменах авторів на крапки з комами, а потім побудує список відокремлених комами записів авторів. Записи авторів буде упорядковано, тому у шаблоні використано 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))

Виведені дані на панелі Подробиці щодо книги виглядатимуть так:

Збережені шаблони

У загальному режимі програмування і режимі шаблонів Python<python_mode> передбачено можливість збереження шаблонів загального режиму програмування та виклику цих шаблонів з інших шаблонів, подібно до виклику збережених функцій. Зберегти шаблони можна за допомогою пункту :guilabel:`Налаштування->Додатково->Шаблонні функції. у відповідному діалоговому вікні буде наведено довідкову інформації. Викликати шаблон можна у той самий спосіб, що і функцію, передавши йому, якщо потрібно, позиційні аргументи. Аргументом може бути будь-який вираз. Приклади виклику шаблону, де ми припускаємо, що збережений шаблон має назву foo:

- foo() — виклик шаблону без передавання аргументів.

- foo(a, b) — виклик шаблону з передаванням значень двох змінних — a і b.

- foo(if field('series') then field('series_index') else 0 fi) — якщо книга належить до циклу, передати номер книги у циклі, інакше — передати значення 0.

Отримання аргументів відбувається за допомогою виклику збереженого шаблона за допомогою функції arguments. Ця функція одразу оголошує і ініціалізує локальні змінні, які виконують роль параметрів. Ці змінні є позиційними — їм надається значення, яке стоїть у виклику на відповідній позиції. Якщо відповідне значення змінної не було надано у виклику, arguments надає параметру типового значення. Якщо типове значення не вказано, змінній буде надано значення порожнього рядка. Наприклад, наведена нижче функція arguments оголошує 2 змінні — key, alternate:

arguments(key, alternate='series')

У наведених нижче прикладах знову припускаємо, що збережений шаблон має назву foo:

- foo('#myseries') — аргумент key матиме значення 'myseries', а аргумент alternate матиме типове значення 'series'.

- foo('series', '#genre') — змінній key буде надано значення 'series', а змінній alternate — значення '#genre'.

- foo() -— змінній key буде надано значення порожнього рядка, а змінній alternate — значення 'series'.

У PTM аргументи буде передано у формі параметра arguments, який є списком рядків. Не існує способу вказати типові значення. Вам слід перевірити довжину списку arguments, щоб переконатися, що кількість аргументів є тією, на яку ви сподіваєтеся.

Простим способом тестування збережених шаблонів є використання вікна Засіб тестування шаблонів. Для полегшення доступу надайте цьому вікну клавіатурного скорочення за допомогою пункту Налаштування → Додатково → Клавіатурні скорочення → Засіб тестування шаблонів. Надання клавіатурного скорочення вікну Збережені шаблони допоможе вам швидше перемикатися між тестуванням та редагування початкового коду збереженого шаблону.

Надання додаткової інформації шаблонам

Розробник може передати додаткові відомості обробнику шаблонів, зокрема, специфічні для книги метадані або відомості про те, що слід виконати обробнику. Шаблон може отримувати доступ до цих відомостей і використовувати їх під час обробки.

Розробник: як передати додаткові відомості

Додатковими відомостями є словник Python, який містить пари назва_змінної: значення_змінної, де значеннями мають бути рядки. Шаблон може отримувати доступ до словника, створюючи локальні змінні із назвами назва_змінної, які міститимуть значення значення_змінної. Користувач не може змінювати назву, тому краще користуватися назвами, які не збігатимуться із локальними змінними інших шаблонів. Можна, наприклад, додати до усіх назв початковий символ підкреслювання.

Цей словник буде передано обробнику шаблонів (форматувальнику) за допомогою іменованого параметра global_vars=ваш_словник. Ось повний запис методу:

def safe_format(self, fmt, kwargs, error_value, book,
column_name=None, template_cache=None,
strip_results=True, template_functions=None,
global_vars={})

Розробник шаблонів: як отримати доступ до додаткових відомостей

Отримати доступ до додаткових відомостей (словника globals) у шаблоні можна за допомогою шаблонної функції:

globals(id[=expression] [, id[=expression]]*)

де записом ідентифікатор є будь-яка коректна назва змінної. Ця функція перевіряє, чи у додаткових відомостях, наданих розробником, міститься відповідна назва. Якщо це так, функція призначає надане значення змінної локальній змінній із вказаною назвою. Якщо назву змінної не буде виявлено у додаткових відомостях і якщо вказано вираз, буде виконано обчислення виразу із призначенням результату локальній змінній. Якщо не буде знайдено ні значення, ні виразу, локальній змінній буде призначено порожній рядок ('').

Шаблон може встановлювати значення у словнику globals за допомогою шаблонної функції:

set_globals(id[=expression] [, id[=expression]]*)

Ця функція встановлює значення пари словника globals ключ:значення ідентифікатор:значення, де значенням є значення локальної змінної шаблона ідентифікатор. Якщо такої локальної змінної не існує, значення буде встановлено для результату обчислення виразу.

Зауваження щодо відмінностей між режимами

У роботі трьох режимів програмування, режиму єдиної функції, режиму програмування шаблонів та загального режиму програмування є відмінності. Режим єдиної функції є «простим», тому у ньому багато можливостей програмування «приховано».

Відмінності:

У режимі єдиної функції значення стовпчика завжди передається як «невидимий» перший аргумент функції, включено до шаблона.

У режимі єдиної функції не передбачено підтримки розрізнення між змінними і рядками; усі значення є рядками.

У наведеному нижче шаблоні режиму єдиної функції шаблон повертає або назву циклу, або рядок «no series»:

{series:ifempty(no series)}

Еквівалентний шаблон у режимі програмування шаблонів:

{series:'ifempty($, 'no series')'}

Еквівалентний шаблон у режимі загального програмування:

program: ifempty(field('series'), 'no series')

Першим аргументом ifempty є значення поля series, а другим аргументом — рядок no series. У режимі єдиної функції перший аргумент, значення поля, передається автоматично (невидимий аргумент).

Декілька шаблонних функцій, зокрема booksize() і current_library_name(), не приймають аргументів. Через наявність невидимого аргументу ви не зможете скористатися цими функціями у режимі єдиної функції.

У режимі єдиної функції не можна скористатися вкладеними функціями, коли одна з функцій викликає іншу функцію для обчислення свого аргументу. Наприклад, цей шаблон, який призначено для отримання перших 5 літер значення назви циклу у верхньому регістрі, не працюватиме у режимі єдиної функції:

{series:uppercase(substr(0,5))}

У шаблонному режимі програмування та загальному режимі програмування передбачено підтримку вкладених функцій. Наведений вище шаблон у шаблонному режимі програмування можна записати так:

{series:'uppercase(substr($, 0,5))'}

У загальному режимі програмування це було б:

program: uppercase(substr(field('series'), 0,5))

Як було зауважено вище у розділі Режим програмування шаблонів, використання символів { і } у рядкових літералах режиму програмування шаблонів може призвести до помилок або неочікуваних результатів, оскільки ці символи заважають нормальній роботі обробника шаблонів. Він намагається обробити їх як межі шаблона, а не символи. У деяких, але не в усіх випадках, ви можете замінити { на [[ і } на ]]. Загалом кажучи, якщо у вашій програмі є символи { і }, вам слід користуватися загальним режимом програмування.

Визначені користувачем шаблонні функції Python

Ви можете додавати власні функції мовою Python до обробника шаблонів. Такими функціями можна скористатися в усіх трьох режимах програмування шаблонів. Додати функції можна за допомогою сторінки Налаштування  →  Додатково  →  Шаблонні функції. У діалоговому вікні, яке буде відкрито програмою, можна знайти відповідні настанови.

Спеціальні нотатки щодо шаблонів збереження або надсилання

До шаблонів, які використовуються для дій зберегти на диск і надіслати на пристрій, застосовується спеціальна обробка. Значення полів буде очищено із заміною символів, які мають спеціальне значення у файлових системах, зокрема символів похилих рисок, на символи підкреслювання. Це означає, що текст поля не можна використовувати для створення тек. Втім, похилі риски не замінюватимуться у рядках префікса і суфікса, отже похилі риски у цих рядках можна використати для створення тек. Таким чином, ви можете створити структуру тек довільного рівня вкладеності.

Наприклад, припустімо ми хочемо створити структуру тек цикл/номер_у_циклі - назва, із умовою, що якщо циклу не існує, назву слід розташувати у верхній теці. Шаблон для виконання цього завдання:

{series:||/}{series_index:|| - }{title}

Похила риска і дефіс мають з’являтися, лише якщо цикл не є порожнім.

Функція lookup надає вам змогу виконувати виконувати навіть ще вишуканішу обробку. Наприклад, припустімо, що якщо книга належить до циклу, ми хочемо мати структуру тек цикл/номер у циклі - назва.формат. Якщо книга не належить до циклу, нам потрібна структура тек жанр/упорядковане ім’я автора/назва.формат. Якщо ж книга не належить до певного жанру, слід скористатися текою „Unknown“. Нам потрібно два повністю різних шляхи, залежно від значення параметра циклу.

Для виконання цього завдання ми:

1. Створимо складене поле (надавши йому назву #aa), що міститиме дані {series}/{series_index} - {title}. Якщо значення циклу (series) є непорожнім, такий шаблон дасть нам цикл/номер_у_циклі - назва.

2. Створимо складене поле (надавши йому назву #bb) із вмістом {#genre:ifempty(Unknown)}/{author_sort}/{title}. Цей шаблон дає жанр/упорядковані_автори/назва. Якщо немає даних щодо жанру, запис жанру буде замінено на Unknown.

3. Встановимо шаблон збереження {series:lookup(.,#aa,#bb)}. Цей шаблон вибирає складене поле #aa, якщо значення запису циклу є непорожнім, і складене поле #bb, якщо значення запису циклу є порожнім. Таким чином, ми матимемо повністю різні шляхи для збереження книг, залежно від того, чи є запис series порожнім.

Підказки

- Для перевірки шаблонів скористайтеся засобом перевірки шаблонів. Додайте пункт засобу до контекстного меню для книг у бібліотеці і/або встановіть для нього клавіатурне скорочення.

- У шаблонах можна використовувати інші шаблони шляхом посилання на складені стовпчики, які побудовано за допомогою бажаних шаблонів. Крім того, ви можете скористатися «Збереженими шаблонами».

- У наборі метаданих ви можете вказати порожні значення поля (або вказати будь-який еквівалент порожнього поля) за допомогою спеціального шаблона {}. Цей шаблон завжди обробляється як порожній рядок.

- Описана вище методика для показу чисел працює зі стандартним полем series_index, навіть якщо вміст відповідного поля є порожнім.

Довідник з функцій

- Reference for all built-in template language functions

- List manipulation

- range(початок, кінець, крок, обмеження) — повертає список, який створено циклічним проходженням діапазону, який вказано параметрами «початок», «кінець» та «крок» із максимальною довжиною «обмеження». Першим значенням у списку буде «початок». Наступними значеннями будуть «наступне_значення = поточне_значення + крок». Проходження циклу триватиме, доки «наступне_значення < кінець» , якщо «крок» є додатним, інакше, доки «наступне_значення > кінець». Буде виведено порожній список, якщо не буде виконано умову «початок >= кінець» і «крок» є додатним. Значення «обмеження» встановлює максимальну довжину списку. Типовим значенням є 1000. Параметри «початок», «крок» і «обмеження» є необов’язковими. Виклик «range()» із одним аргументом задає «кінець». Два аргументи задають «початок» і «кінець». Три аргументи задають «початок», «кінець» і «крок». Чотири аргументи задають «початок», «кінець», «крок» і «обмеження». Приклади: range(5)

- Relational

- strcmpcase(x, y, lt, eq, gt)

- API of the Metadata objects

- Metadata

- STANDARD_METADATA_FIELDS

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Мова шаблонів 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.

- List manipulation

- range(початок, кінець, крок, обмеження) — повертає список, який створено циклічним проходженням діапазону, який вказано параметрами «початок», «кінець» та «крок» із максимальною довжиною «обмеження». Першим значенням у списку буде «початок». Наступними значеннями будуть «наступне_значення = поточне_значення + крок». Проходження циклу триватиме, доки «наступне_значення < кінець» , якщо «крок» є додатним, інакше, доки «наступне_значення > кінець». Буде виведено порожній список, якщо не буде виконано умову «початок >= кінець» і «крок» є додатним. Значення «обмеження» встановлює максимальну довжину списку. Типовим значенням є 1000. Параметри «початок», «крок» і «обмеження» є необов’язковими. Виклик «range()» із одним аргументом задає «кінець». Два аргументи задають «початок» і «кінець». Три аргументи задають «початок», «кінець» і «крок». Чотири аргументи задають «початок», «кінець», «крок» і «обмеження». Приклади: range(5)

- Relational

- strcmpcase(x, y, lt, eq, gt)

- API of the Metadata objects

List manipulation

range(початок, кінець, крок, обмеження) — повертає список, який створено циклічним проходженням діапазону, який вказано параметрами «початок», «кінець» та «крок» із максимальною довжиною «обмеження». Першим значенням у списку буде «початок». Наступними значеннями будуть «наступне_значення = поточне_значення + крок». Проходження циклу триватиме, доки «наступне_значення < кінець» , якщо «крок» є додатним, інакше, доки «наступне_значення > кінець». Буде виведено порожній список, якщо не буде виконано умову «початок >= кінець» і «крок» є додатним. Значення «обмеження» встановлює максимальну довжину списку. Типовим значенням є 1000. Параметри «початок», «крок» і «обмеження» є необов’язковими. Виклик «range()» із одним аргументом задає «кінець». Два аргументи задають «початок» і «кінець». Три аргументи задають «початок», «кінець» і «крок». Чотири аргументи задають «початок», «кінець», «крок» і «обмеження». Приклади: range(5)

class calibre.utils.formatter_functions.BuiltinRange[source]

range(початок, кінець, крок, обмеження) — повертає список, який створено циклічним проходженням діапазону, який вказано параметрами «початок», «кінець» та «крок» із максимальною довжиною «обмеження». Першим значенням у списку буде «початок». Наступними значеннями будуть «наступне_значення = поточне_значення + крок». Проходження циклу триватиме, доки «наступне_значення < кінець» , якщо «крок» є додатним, інакше, доки «наступне_значення > кінець». Буде виведено порожній список, якщо не буде виконано умову «початок >= кінець» і «крок» є додатним. Значення «обмеження» встановлює максимальну довжину списку. Типовим значенням є 1000. Параметри «початок», «крок» і «обмеження» є необов’язковими. Виклик «range()» із одним аргументом задає «кінець». Два аргументи задають «початок» і «кінець». Три аргументи задають «початок», «кінець» і «крок». Чотири аргументи задають «початок», «кінець», «крок» і «обмеження». Приклади: 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)->помилка(перевищено обмеження).

Relational

strcmpcase(x, y, lt, eq, gt)

class calibre.utils.formatter_functions.BuiltinStrcmpcase[source]

strcmpcase(x, y, lt, eq, gt) – виконує порівняння x і y, як рядків, із врахуванням регістру символів. Повертає lt, якщо x < y. Повертає eq, якщо x == y. У інших випадках повертає gt.
Зауваження: це НЕ типова поведінка, яку використовує calibre, наприклад, у операторах лексичного порівняння (==, >, <, тощо). Використання цієї функції може призвести до неочікуваних результатів, бажано користуватися, якщо це можливо, strcmp().

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=('Невідомий',), 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »

- Все про використання формальних виразів в calibre

Все про використання формальних виразів в calibre

Формальні вирази використовуються у багатьох частинах calibre для виконання витонченої обробки вмісту та метаданих електронної книги. Ця частина підручника є лише початковими настановами, які допоможуть вам розпочати роботу із використанням формальних виразів у calibre.

Зміст

- Спочатку, декілька попереджень і декілька заохочень

- Де в calibre ви можете використовувати формальні вирази?

- Що ж таке ці «формальні вирази»?

- Трохи пояснень

- Звучить непогано. Що далі?

- Ого, чудово! Це може бути корисним!

- Добре, ці спеціальні символи корисні і усе таке, але що, якщо потрібно знайти крапку або знак питання?

- Отже, якими є найкорисніші набори?

- Що якщо потрібно знайти якийсь із декількох рядків? Все значно ускладниться?

- Ви пропустили…

- На початку була згадка про те, що формальні вирази можна зробити незалежними від регістру символів. Як це зробити?

- Здається, починаю розуміти для чого усі ці формальні вирази… Але як ними скористатися у calibre?

- Перетворення

- Додавання книг

- Пакетне редагування метаданих

- Короткий довідник

- Подяки

Спочатку, декілька попереджень і декілька заохочень

Пояснення, і цього не уникнути, будуть дещо технічними. Щоб там не було, але формальні вирази є технічним інструментом для виконання технічних завдань. Буде використано певний жаргон та поняття, які можуть здатися складними або заплутаними. Ми спробуємо надати якомога прозоріші пояснення, але без цього жаргону і відповідних понять не обійтися. Втім, не лякайтеся жаргону, ми спробуємо пояснити усі нові речі. І хоча самі формальні вирази можуть здатися загадковими закляттями (або, якщо прозаїчніше, рядками з випадкових символів, дивною сумішшю літер із знаків), ми вам обіцяємо — все не так уже і складно. Навіть ті, хто добре знається на формальних виразах, іноді не одразу розуміють призначення складних формальних виразів, але написати формальний вираз не так уже і складно — він конструюється покроково. Отже, почнімо і зробимо перший крок до цієї кролячої нори.

Де в calibre ви можете використовувати формальні вирази?

Формальні вирази використовуються у calibre у декількох місцях. Це, зокрема, засіб пошуку із заміною у параметрах перетворення книг, засіб визначення метаданих за назвами файлів у параметрах імпортування та засіб пошуку із заміною у інструменті пакетного редагування метаданих книг. У засобі пошуку із заміною редактора книг calibre також можна використовувати формальні вирази. Нарешті, ви можете скористатися формальними виразами для пошуку у списку книг calibre та для пошуку у засобі перегляду книг calibre.

Що ж таке ці «формальні вирази»?

Формальний вираз — це спосіб описати певний набір рядків. Один формальний вираз може відповідати декільком різним рядкам. Це саме те, що робить формальні вирази такими потужними — вони є способом коротко описати велику кількість можливих варіантів рядка.

Примітка

Тут термін «рядок» використано у сенсі, у якому його використовують у мовах програмування. Це упорядкована сукупність одного або декількох символів. Символами вважаються літери, цифри, символи пунктуації, а також так звані пробіли (символи розриву рядків, табуляції тощо). Будь ласка, зауважте, що, загалом кажучи, літери верхнього регістру (великі літери) і літери нижнього регістру (малі літери) вважаються різними знаками. Отже «a» — це не та сама літера, що «A». У calibre регістр символів не береться до уваги на панелі пошуку, але враховується у параметрах перетворення книг. Існує спосіб зробити будь-який формальний вираз таким, що обробляється без врахування регістру символів, але цей спосіб ми обговоримо пізніше. Неврахування регістру символів дещо ускладнює ситуацію, оскільки формальні вирази уможливлюють варіації відповідників, отже одному формальному виразу відповідає одразу декілька рядків. Ось чому дехто взагалі не бажає користуватися формальними виразами. Докладніше про це трохи згодом.

Трохи пояснень

Ну, гаразд, для цього ми і почали це обговорення. По-перше, ось найважливіша концепція у формальних виразах: рядок є формальним виразом, який відповідає самому собі. Тобто, якщо ви хочете знайти рядок «Привіт, світе!» за допомогою формального виразу, формальним виразом, яким ви маєте скористатися є рядок «Привіт, світе!». Так, це справді дуже просто. Втім, варто зауважити, що під час пошуку за цим рядком буде знайдено лише рядок «Привіт, світе!», а не, наприклад, рядки «Привіт, сВіте!» чи «привіт, світе!».

Звучить непогано. Що далі?

Далі, ми розпочнемо знайомитися зі справді корисними речами. Пам’ятаєте, ми згадували про те, що один формальний вираз може відповідати декільком рядкам? Тут ми обговоримо трохи складніші речі. Припустімо для нашого дещо практичнішого прикладу, що у електронній книзі, яку ви хочете перетворити є небажані рядки у нижньому колонтитулі, які вказують на номер сторінки, наприклад «Сторінка 5 з 423». Очевидно, номери сторінок мають зростати від 1 до 423, отже, нам доведеться створити відповідники 423 різних рядків, чи не так? Ні, не так: формальний вираз надасть нам змогу визначити усі можливі набори символів для відповідності. Щоб визначити набір, слід вказати усі його символи у квадратних дужках. Наприклад, набір [abc] відповідає літері «a», «b» або «c». Наборам завжди відповідає один із символів набору. Передбачено можливість вказування діапазонів символів, наприклад, якщо вам потрібно вказати усі латинські літери нижнього регістру, вам слід скористатися набором [a-z], для латинських літер нижнього і верхнього регістрів — набором [a-zA-Z] тощо. Вловили ідею? Отже, очевидно, використавши вираз Сторінка [0-9] з 423, ми зможемо встановити відповідність першим дев’ятьом сторінкам, таким чином зменшивши кількість потрібних для встановлення відповідності виразів до трьох: другим виразом буде Сторінка [0-9][0-9] з 423 (він відповідатиме двоцифровим номерам сторінок), а про третій ви можете здогадатися самі. Так, вгадайте його і запишіть.

Ого, чудово! Це може бути корисним!

Ми сподівалися, що ви скажете саме це. Але зачекайте, зараз буде ще краще! Щойно ми використали набори для встановлення відповідності одного з декількох символів. Але можна повторити символ або набір і заощадити кількість виразів потрібних для обробки номерів сторінок у наведеному вище прикладі. Так, достатньо усього одного виразу! Вражені? Ну, звичайно ж! Це працює ось так: передбачено так звані спеціальні символи, «+», «?» і «*», для повторення одинарного елемента, який безпосередньо передує цим символам. (Елементом може бути окремий символ, набір символів, керівна послідовність символів або група (опис останніх двох наведено нижче) — якщо коротко, елементом може бути будь-який окремий об’єкт у формальному виразі). Ці символи називаються замінниками або кванторами. Якщо бути точнішим, «?» відповідає 0 або 1 попередньому елементу, «*» відповідає 0 або більшій кількості попереднього елемента, а «+» відповідає 1 або більшій кількості попереднього елемента. Декілька прикладів: вираз a? буде відповідати «» (тобто порожньому рядку, що не дуже корисно у нашому прикладі) або «a»; вираз a* відповідає «», «a», «aa» або будь-якій кількості послідовних літер «a», і, нарешті, вираз a+ відповідає «a», «aa» або будь-якій кількості послідовних літер «a» (Зауваження: останній вираз не відповідає порожньому рядку!). Те саме стосується і наборів: вираз [0-9]+ відповідатиме будь-якому цілому числу! Знаємо, про що ви подумали, і ви праві: якщо ми розглядатимемо наведений вище приклад із номерами сторінок, чи не буде це саме той єдиний вираз, який відповідає усім номерам сторінок? Так, вираз Сторінка [0-9]+ з 423 відповідатиме будь-якій сторінці у книзі!

Примітка

Зауваження щодо цих кванторів: загалом кажучи, при використанні кванторів програма намагатиметься використати найбільший із можливих відповідників, отже, вам слід бути обережними з використанням кванторів. Така поведінка називається «жадібністю», думаємо, ви зрозуміли чому. Вона може призводити до певних проблем, зокрема під час пошуку теґів. Розгляньмо, наприклад, рядок "<p class="calibre2">Заголовок</p>". Скажімо, вам потрібно знайти у ньому початковий теґ (частину між першою паролю кутових дужок, докладніше про теґи нижче). Можна припустити, що вираз <p.*> відповідатиме теґові, але насправді він відповідає усьому рядку! (Символ «.» є ще одним спеціальним символом. Він відповідає будь-якому символу окрім символів розриву рядка, отже, загалом, вираз .* відповідає будь-якому окремому рядку.) Замість цього виразу слід скористатися виразом <p.*?>, який робить квантор * нежадібним. Такий вираз відповідає лише першому початковому теґу, як і планувалося. Насправді, існує інший спосіб досягти потрібного результату: вираз <p[^>]*> також відповідає початковому теґу. Чому так? Про це ви дізнаєтеся із наступного розділу. Зауважте лише, що зазвичай існує принаймні декілька способів запису логіки одного формального виразу.

Добре, ці спеціальні символи корисні і усе таке, але що, якщо потрібно знайти крапку або знак питання?

Звичайно ж, існує спосіб зробити це: достатньо поставити символ зворотної похилої риски перед спеціальним символом, і він стане звичайним символом без додаткових значень. Пара символів зворотної похилої риски із наступним символом називається керівною послідовністю, а додавання символу зворотної похилої риски перед спеціальним символом називається екрануванням символу. Екранована послідовність символів вважається єдиним елементом формального виразу. Звичайно ж, існують керівні послідовності, які означають більше ніж просте екранування спеціальних символів, наприклад, \t означає символ табуляції. Деякі з таких керівних послідовностей ми розглянемо нижче. О, і до речі, щодо спеціальних символів: усі символи, які ми обговорювали у цьому вступі як такі, що мають особливе призначення, потребують екранування, якщо ви хочете скористатися ними як звичайними символами.

Отже, якими є найкорисніші набори?

Ми знали, що ви про це запитаєте. Серед корисних наборів символів є набір [0-9], який відповідає будь-якій цифрі, набір [a-z], який відповідає будь-якій латинській літері нижнього регістру, набір [A-Z], який відповідає будь-якій латинській літері верхнього регістру, набір [a-zA-Z], який відповідає будь-якій латинській літері, та набір [a-zA-Z0-9], який відповідає будь-якій латинській літері або цифрі. Ви також можете використовувати такі екрановані послідовності:

\d

є рівнозначним [0-9]

\w

є рівнозначним [a-zA-Z0-9_]

\s

є рівнозначним будь-якому пробілу

Примітка

«Пробілами» вважаються будь-які символи, які не можна надрукувати. Такими символами є звичайні пробіли, символи табуляції, символи заповнення рядка, заповнення форми, символи повернення каретки, нерозривні пробіли тощо.

Примітка

Пошук можна виконувати за літерами будь-якого регістру, якщо позначено пункт, який робить пошук нечутливим до регістру. Такі пункти, можна знайти, наприклад, на сторінці «Налаштування->Пошук» у guilabel:Переглядачі книг calibre, також засобі Редагувати книгу.

На завершення розмови про набори, — ви також можете визначити набір як будь-який символ, окрім символів у вказаному наборі. Для цього слід додати символ ^ як найперший символ у наборі. Таким чином, [^a] відповідає будь-якому символу, окрім символу «a». Така дія називається доповненням набору. Керівні послідовності, які ми розглядали вище, також можна використовувати для доповнення: \D відповідає будь-якому нецифровому символу, отже, є еквівалентом [^0-9]. Інші скорочені форми доповнень, як ви вже могли здогадатися, створюються використанням літери верхнього регістру замість літери нижнього регістру. Отже, повертаючись до прикладу із <p[^>]*> з попереднього розділу, можемо бачити, що набір символів, який у ньому використано, відповідає будь-якому символу, окрім кінцевої кутової дужки.

Що якщо потрібно знайти якийсь із декількох рядків? Все значно ускладниться?

Не бійтеся, життя є простим і прекрасним. Розгляньмо приклад: на сторінках книги, перетворення якої ви хочете виконати, назву книги написано у верхньому колонтитулі кожної непарної сторінки, а імена авторів — у верхньому колонтитулі парних сторінок. Друкована книга виглядає чудово, але для електронної книги усі ці дані є зайвими. Ви можете згрупувати вирази для колонтитулів у звичайних круглих дужках і скористатися символом | для встановлення відповідності будь-якому з виразів, які розташовано з обох боків від цього символу. Поєднайте рядки для парних і непарних сторінок, і справу буде зроблено. Надто швидко для вас, не зрозуміли? Гаразд, спочатку ми згрупуємо вирази для непарних і парних сторінок, отримавши (Назва)(Автор) для двох відповідних виразів. Далі, спростимо речі, скориставшись символом вертикальної риски (| називається символом вертикальної риски): якщо буде використано вираз (Назва|Автор) під час пошуку відповідність встановлюватиметься або для рядка «Назва» (на непарних сторінках) або для рядка «Автор» (на парних сторінках). Гаразд, хіба це не просто?

Звичайно ж, ви можете скористатися вертикальною рискою без групування за допомогою дужок. Пам’ятаєте, ми згадували, що квантори повторюють елемент, який їм передує? Гаразд, вертикальна риска працює трохи інакше: вираз «Назва|Автор» також відповідає або рядку «Назва», або рядку «Автор», як і у наведеному вище прикладі із групуванням. За допомогою вертикальної риски можна зробити варіантами для вибору вираз, який передує їй і вираз, який розташовано після вертикальної риски. Отже, якщо ви хочете знайти рядок «Calibre» і рядок «calibre», де змінюється лише регістр початкової літери «c», варто скористатися виразом (c|C)alibre, де групування забезпечує вибір лише між різними регістрами літери «c». Якщо б ви використали вираз c|Calibre, варіантами б були рядки «c» і «Calibre», тобто не ті рядки, які нам потрібні. Якщо коротко: не бійтеся використовувати групування для того, щоб уникнути двозначності у виразі.

Ви пропустили…

…зачекайте, ще одна остання дуже цікава річ, пов’язана із групами. Якщо у виразі є група, з якою встановлюється відповідність рядка пошуку, ви можете посилатися на цю групу далі у виразі. Групи нумеруються, починаючи з 1, а посилаються на них додаючи символ екранування до номера групи. Отже, на п’яту групу посилаються за допомогою послідовності \5. Таким чином, якщо виконуватиметься пошук за виразом ([^ ]+) \1 у рядку «Тест Тест», буде встановлено відповідність формального виразу усьому рядку!

На початку була згадка про те, що формальні вирази можна зробити незалежними від регістру символів. Як це зробити?

Так, ми про це згадували. Дякуємо, що були уважними і нагадали про це. Ви можете повідомити calibre про те, як обробляти певні речі, за допомогою так званих прапорців. Включити прапорці до вашого виразу можна за допомогою спеціальної конструкції (?тут вказують прапорці), у якій, звичайно ж, вам слід замінити рядок «тут вказують прапорці» на потрібний вам вираз прапорців. Якщо слід ігнорувати регістр символів, маємо вказати прапорець i, тобто включити (?i) до вашого виразу. Таким чином, за допомогою виразу (?i)тест можна знайти рядки «Тест», «тЕст», «ТЕст» та усі інші рядки, які утворюються зміною регістру символів з рядка «тест».

Ще один корисний прапорець надає змогу наказати програмі вважати крапку будь-яким символом, включно із символом розриву рядка. Це прапорець s. Якщо ви хочете використати у виразі декілька прапорців, просто напишіть їх поряд у одній інструкції: використання (?is) призведе до ігнорування регістру символів і заміни крапки будь-яким символом. Не має значення, який із прапорців ви вказали першим — вираз (?si) є рівнозначним до виразу (?is).

Здається, починаю розуміти для чого усі ці формальні вирази… Але як ними скористатися у calibre?

Перетворення

Почнімо з параметрів перетворення, які є добре продуманими. На панелі пошуку з заміною ви можете ввести формальний вираз, який описує рядок, який буде замінено під час перетворення. Найпродуманішою частиною діалогового вікна є майстер перетворення. Після натискання кнопки майстра ви зможете переглянути, як для calibre «виглядає» процедура перетворення. Погортайте текст до рядка, який ви хочете вилучити, позначте його і скопіюйте. Вставте його до поля формального виразу у верхній частині вікна. Якщо у ньому мають бути змінні частини, наприклад номери сторінок, скористайтеся наборами та кванторами для заміни цих частин. Роблячи це, не забувайте про екранування спеціальних символів, якщо вони є у виразі. Натисніть кнопку з міткою Тест, і calibre позначить фрагменти тексту, які було б замінено, якби було використано формальний вираз. Якщо все гаразд, натисніть кнопку Гаразд і виконайте перетворення. Будьте обережні, якщо у початковому тексті містяться мітки, як у цьому прикладі:

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

(безсовісно запозичено з цього обговорення [https://www.mobileread.com/forums/showthread.php?t=75594"]). Вам варто також вилучити і ці теґи. У цьому прикладі рекомендуємо почати з теґу <b class="calibre2">. Далі, вам слід розібратися із відповідним кінцевим теґом (початковими теґами є теґи <теґ>, а кінцевими теґами є теґи </теґ>), яким у нашому прикладі є теґи </b>. (Зверніться до підручника з HTML або запитайте на форумі, якщо маєте якісь питання щодо теґів.) Початковий теґ можна описати як <b.*?>, а кінцевий теґ — </b>. Таким чином, ми можемо вилучити усі дані між цими двома теґами за допомогою виразу <b.*?>.*?</b>. Але не варто користуватися цим виразом, оскільки його використання призведе до вилучення усіх даних у теґах <b> (який, до речі, використовується для позначення тексту напівжирною гарнітурою). Можна закладатися, що таким чином ми вилучимо частину тексту книги. Замість цього, варто включити початок рядка у тезі, створивши формальний вираз <b.*?>\s*Generated\s+by\s+ABC\s+Amber\s+LIT.*?</b> Керівну послідовність \s із кванторами використано замість явного використання пробілів, щоб знайти усі варіанти рядка, які може бути використано у тексті книги. Не забудьте, перевірити, що саме calibre вилучить, щоб переконатися, що не буде вилучено нічого зайвого, виконавши тестування виразу. Якщо ви перевірите лише один відповідник формального виразу, може так статися, що небажаний ефект все ж трапиться у якомусь іншому місці тексту. Також зауважте, що якщо випадково буде вилучено більше або менше теґів, ніж ви хотіли б вилучити, calibre спробує виправити некоректний код після вилучення.

Додавання книг

Ще одним випадком використання формальних виразів є видобування метаданих з назв файлів. Доступ до цієї можливості здійснюється за допомогою розділу «Додавання книг» параметрів програми. Тут є особлива можливість: ви можете скористатися назвами полів метаданих, наприклад, (?P<title>) позначає, що calibre має використати цю частину рядка як назву (заголовок) книги. Доступні назви полів показано у списку разом із ще одним чудовим полем для тестування. Приклад: припустімо, ви хочете імпортувати багато файлів із назвами, які подібні до Класика: Божественна комедія написана Данте Аліг’єрі.mobi (звичайно ж, ця книга вже є у вашій бібліотеці, оскільки усі ми закохані у італійську поезію) або Класика наукової фантастики: Трилогія Фундація написана Айзек Азімов.epub. Очевидно, така схема найменування не дасть типовій програмі calibre видобути корисні дані — стандартним є такий вираз для видобування метаданих: (?P<title>.+) - (?P<author>[^_]+). Формальним виразом, який спрацює є такий: [а-яґіїєА-ЯҐІЇЄ]+: (?P<title>.+) написана (?P<author>.+). Будь ласка, зауважте, що всередині групи для поля метаданих вам слід скористатися виразами для опису відповідного поля. Також зауважте, що при використанні поля тестування calibre вам слід додати суфікс назви файла до тестової назви, інакше відповідників не буде знайдено взагалі, хоча вираз і буде працездатним.

Пакетне редагування метаданих

Останнім розглянемо використання формальних виразів для пошуку із заміною у полях метаданих. Отримати доступ до цього інструмента можна позначивши декілька книг у списку бібліотеки і натиснувши кнопку пакетного редагування метаданих. Будьте обережні з використанням пакетного редагування, оскільки помилки у ньому можуть призвести до дуже поганих наслідків для вашої бібліотеки! Двічі перевірте, чи правильно працюють ваші вирази, за допомогою полів тестування. Позначайте лише ті книги, які дійсно потребують змін! У режимі пошуку за формальними виразами ви можете виконувати пошук у одному полі, замінювати чимось текст і навіть записувати результат заміни до іншого поля. Приклад з практики: припустімо, у вашій бібліотеці є книги з циклу «Дюна» Френка Герберта, як названо так: Дюна 1 - Дюна, Дюна 2 - Месія Дюни тощо. Вам потрібно записати у поле циклу рядок Дюна. Зробити це можна, виконавши пошук (.*?) \d+ - .* у полі назви книги із заміною вмісту поля циклу на \1. Зрозуміли, як усе зроблено? Ми замінюємо вміст поля циклу на посилання на першу групу з поля назви. Тепер, коли дані записано до поля циклу, можна виконати ще один пошук .*? - у полі назви і замінити знайдену назву циклу на "" (порожній рядок). Після цього метадані ваших книг набудуть послідовного та компактного вигляду. Хіба це не чудово? До речі, замість заміни усього поля, ви можете дописати до його поточного вмісту дані на початку або наприкінці. Отже, якщо ви хочете дописати перед назвою книги назву циклу, це також можна зробити. Як ви вже, поза всяким сумнівом, знаєте, у вікні є пункт З урахуванням регістру, отже для зміни поведінки засобу обробки формальних виразів немає потреби у використанні прапорців.

Гаразд, на цьому можна і завершити дуже короткий вступ щодо формальних виразів. Сподіваємося, ми показали вам достатньо, щоб принаймні створити початкові враження і уможливити ваше подальше самостійне навчання. Продовжити вивчення формальних виразів можна за допомогою документації з формальних виразів у Python [https://docs.python.org/library/re.html].

Втім, наостанок маємо вас попередити: формальні вирази є потужним, але дуже примхливим інструментом. У calibre передбачено справді чудові можливості для перегляду результатів роботи ваших виразів. Скористайтеся ними. Намагайтеся не прострелити собі ногу (Боже, як мені подобається цей вираз…) Якщо ж, попри це попередження, трапляться якісь неприємності, лікуйте вашу ногу (або інші частини тіла), намагайтеся вивчити навчитися і з невдач.

Короткий довідник

- Короткий довідник щодо синтаксису формальних виразів

- Класи символів
- Скорочення для класів символів
- Лічильники
- Жадібність
- Альтернативи
- Виключення
- Прив’язки
- Групи
- Перегляди вперед і назад
- Рекурсія
- Спеціальні символи
- Метасимволи
- Режими

Подяки

Дякуємо за підказки, виправлення та настанови:

- ldolse

- kovidgoyal

- chaley

- dwanthny

- kacir

- Starson17

- Orpheu

Докладніші відомості щодо формальних виразів можна знайти у Підручнику користувача Python [https://docs.python.org/library/re.html]. Бібліотекою, яку calibre використовує для роботи з формальними виразами, є regex [https://bitbucket.org/mrabarnett/mrab-regex/src/hg/] У цій бібліотеці реалізовано підтримку декількох корисних удосконалень стандартної бібліотеки Python.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »

- Все про використання формальних виразів в calibre »

- Короткий довідник щодо синтаксису формальних виразів

Короткий довідник щодо синтаксису формальних виразів

У наведеному тут списку зібрано найпоширеніші і найважчі у запам’ятовуванні частини синтаксису рушія формальних виразів, який використовується для реалізації можливостей із пошуку з заміною у більшості частин calibre.

Зміст

- Класи символів

- Скорочення для класів символів

- Лічильники

- Жадібність

- Альтернативи

- Виключення

- Прив’язки

- Групи

- Перегляди вперед і назад

- Рекурсія

- Спеціальні символи

- Метасимволи

- Режими

Класи символів

Класи символів корисні для скороченого представлення різних груп символів.

Приклади:

Представлення

Клас

[a-z]

Літери нижнього регістру. Без символів із акцентами та лігатур.

[a-z0-9]

Літери нижнього регістру від a до z або цифри від 0 до 9.

[A-Za-z-]

Літери верхнього або нижнього регістру чи дефіс. Щоб включи дефіс до класу, вам слід розташувати його на початку або наприкінці класу, щоб рушій не сплутав його із дефісом, який визначає діапазон символів.

[^0-9]

Будь-який символ, окрім цифри. Символ каре (^), розташований на початку класу, виключає символи з класу (формує доповнення до класу).

[[a-z]--[aeiouy]]

Приголосні нижнього регістру. Клас можна включати до іншого класу. Символи -- виключають клас, який слідує за ними.

[\w--[\d_]]

Усі літери (разом із літерами із акцентами і літерами будь-яких абеток). Скорочення класів можна використовувати всередині класу.

Приклад:

<[^<>]+> to select an HTML tag

Скорочення для класів символів

Представлення

Клас

\d

Цифра (те саме, що і [0-9])

\D

Будь-який нечисловий символ (те саме, що і [^0-9])

\w

Буквенно-цифровий символ ([a-zA-Z0-9]), включно із символами із акцентами та лігатурами.

\W

Будь-який символ, який не є частиною слів.

\s

Пробіл, нерозривний пробіл, табуляція або символ розриву рядка.

\S

Будь-який символ, який не є пробілом

.

Будь-який символ, окрім символу розриву рядка. Скористайтеся пунктом «крапка — усі» або модифікатором формального виразу (?s), щоб включити до класу символ розриву рядка.

Лічильники

Лічильник

Кількість входжень підвиразу, який передує лічильнику, до виразу

?

0 або 1 входження до виразу. Те саме, що {0,1}

+

1 або більше входжень підвиразу. Те саме, що {1,}

*

0, 1 або більше входжень підвиразу. Те саме, що {0,}

{n}

Точно n входжень підвиразу

{min,max}

Кількість входжень між мінімальним і максимальним значенням.

{min,}

Кількість входжень, що перевищує мінімальне значення або дорівнює йому.

{,max}

Кількість входжень між 0 і максимальним значенням, включно.

Жадібність

Типово, якщо вказано лічильники, обробка формального виразу відбувається у «жадібному» режимі: встановлюється відповідність якомога більшому фрагменту. На початку, це може видатися доволі дивним. ? додається до лічильника, щоб зробити його «лінивим». Використовуйте не більше одного такого «лінивого» лічильника у виразі, оскільки результати використання декількох можуть бути доволі несподіваними.

Слід пам’ятати, що варто уникати створення вкладеностей у визначеннях, наприклад вживання взірців, подібних до (a*)*, оскільки це експоненційно збільшує час обробки.

Альтернативи

Символ | у формальному виразі означає логічне АБО. Він означає, що відповідність може бути встановлено за частиною, яка йому передує, або за частиною, яка за ним слідує.

Виключення

Метод 1
взірець_для_виключення(*SKIP)(*FAIL)|взірець_для_відповідності

Приклад:

"Blabla"(*SKIP)(*FAIL)|Blabla
встановлює відповідність за виразом Blabla, у рядку Blabla або «Blabla or Blabla», але не у рядку «Blabla».

Метод 2
взірець_для_виключення\K|(взірець_для_відповідності)

"Blabla"\K|(Blabla)

встановлює відповідність за виразом Blabla, у рядку Blabla або «Blabla or Blabla», але не у рядку «Blabla».

Прив’язки

Прив’язка використовується для визначення логічного розташування у рядку, а не для пошуку якогось символу. Найкориснішими прив’язками для обробки текстових даних є такі:

\b

Позначає межу слова, тобто перехід від пробільних до непробільних символів, і навпаки. Наприклад, ви можете скористатися виразом \bsurd, щоб знайти the surd, але при цьому пропустити слово absurd.

^

Відповідає початку рядка (у багаторядковому режимі, який є типовим).

$

Відповідає кінцю рядка (у багаторядковому режимі, який є типовим).

\K

Скидає початок відповідника до його початку у взірці. У деяких рушіях обробки формальних виразів (але не у calibre) заборонений перегляд назад на змінну довжину, особливо, якщо використано лічильник. Ви можете скористатися \K для таких рушіїв. Це також дозволить обійти обмеження написанням еквівалентного додатного перегляду назад змінної довжини.

Групи

(вираз)

Захоплена група, яка зберігає рядок-відповідник і яку можна згодом використовувати у взірцях пошуку або заміни за допомогою виразу \n, де n є номером групи (нумерація починається з 1 у напрямку читання).

(?:вираз)

Група, яка не містить відповідник.

(?>вираз)

Атомарна група: якщо буде встановлено відповідність виразу, рушій обробки формальних виразів продовжить обробку решти взірця. Якщо під час цієї обробки загальну відповідність встановити не вдасться, рушій не повертатиметься до атомарної групи, щоб спробувати інші комбінації з виразом. Атомарні групи не можна захоплювати.

(?|вираз)

Розгалуження групи: гілки альтернативних варіантів буде включено до виразу із однаковими номерами груп.

(?<назва>вираз)

Група з назвою «назва». Відповідник групи далі можна використати у взірці пошуку так: (?P=назва), у заміні так: \g<назва>. Дві різні групи можуть використовувати одну назву.

Перегляди вперед і назад

Перегляд

Значення

?=

Додатний перегляд вперед (слід розташовувати після відповідника)

?!

Від’ємний перегляд назад (слід розташовувати після відповідника)

?<=

Додатний перегляд назад (слід розташовувати перед відповідником)

?<!

Від’ємний перегляд назад (слід розташовувати перед відповідником)

Самі перегляди вперед і назад не містять жодного символу, вони є рядками нульової довжини і не захоплюються рушієм. Вони є атомарними групами: щойно буде встановлено відповідність, рушій обробки формальних виразів продовжить обробку виразу, якщо встановити відповідність за рештою виразу не вдасться, рушій уже не повертатиметься до перегляду, щоб спробувати іншу комбінацію.

Якщо відбувається пошук декількох відповідників у тексті, на початковій позиції кожної спроби встановлення відповідності, перегляд назад може виконувати вивчення символів до поточної позиції. Тому у рядку 123 взірець (?<=\d)\d (цифра перед цифрою) має, теоретично, відповідати підрядкам 2 і 3. З іншого боку, взірець \d\K\d відповідає лише підрядку 2, оскільки початкова позиція після першого відповідника розташована безпосередньо перед підрядком 3, а там недостатньо символів для встановлення відповідності другого підрядка. Так само, \d(\d) захоплює лише 2. Практично, у рушії обробки формальних виразів calibre, позитивний пошук назад працює так само, і встановлює відповідність лише підрядка 2, на відміну від теорії.

Групи можна розташовувати у переглядах, але захоплення у таких випадках буде корисним доволі нечасто. Як би там не було, якщо виникне потреба у ньому, слід бути дуже обережним у використанні лічильників у переглядах назад: «жадібність», пов’язана із тим, що немає зворотного стеження, може призвести до несподіваних результатів захоплення. З цієї причини варто користуватися \K замість додатного перегляду назад, якщо у вас є лічильник (або, ще гірше, декілька) у групі захоплення додатного перегляду назад.

Приклад від’ємного перегляду вперед:

(?![^<>{}]*[>}])

Якщо розташувати цей вираз наприкінці взірця, він запобігатиме встановленню відповідності усередині теґу або стилю, вбудованого до файла.

Якщо це можливо, завжди варто «прив’язувати» перегляди, щоб зменшити кількість кроків, які потрібні для досягнення результату.

Рекурсія

Представлення

Значення

(?R)

Рекурсія за цілим взірцем

(?1)

Рекурсія єдиного взірця нумерованої групи, у нашому прикладі, групи 1.

Рекурсія викликає сама себе. Рекурсії зручно використовувати для запитів із балансом певних символів, зокрема рядків у лапках, які можуть містити всередині інші рядки у лапках. Таким чином, якщо під час обробки рядка у подвійних лапках нам трапиться початок підрядка у подвійних лапках, ми просто викличемо для обробки рекурсію. Взірець можна скласти так:

start-pattern(?>atomic sub-pattern|(?R))*end-pattern

Для встановлення відповідності рядка між символами подвійних лапок, не зупиняючись на вбудованому рядку:

“((?>[^“”]+|(?R))*[^“”]+)”

Цим шаблоном можна також скористатися для внесення змін до пар теґів, які можна вбудовувати, зокрема теґів <div>.

Спеціальні символи

Представлення

Символ

\t

табуляція

\n

розрив рядка

\x20

(розривний) пробіл

\xa0

нерозривний пробіл

Метасимволи

Метасимволи — це символи, які мають особливе значення для рушія обробки формальних виразів. Дванадцять із метасимволів має бути екрановано символом зворотної похилої риски (\), щоб вони втратили своє особливе значення і стали звичайними символами у взірці:

^ . [ ] $ ( ) * + ? | \

Сім інших метасимволів не потребують екранування символом зворотної похилої риски (втім, їх може бути екрановано без будь-яких наслідків):

{ } ! < > = :

Спеціальні символи втрачають свій статус, якщо їх використано у класі (між квадратними дужками, []). Кінцева дужка і дефіс мають особливий статус у межах визначення класу. Поза визначенням класу дефіс є звичайним дефісом, а кінцева квадратна дужка лишається метасимволом.

Символ похилої риски (/) і символ номера (або символ-хеш) (#) не є метасимволами. Потреби у їхньому екрануванні немає.

У деяких інструментах, зокрема regex101.com, де використовується рушій Python, подвійні лапки мають особливе значення як роздільник, тому їх слід екранувати або вдаватися до зміни параметрів роботи рушія. У редакторі calibre подвійні лапки не мають особливого значення.

Режими

(?s)

Робить так, що крапка (.) вважатиметься відповідником будь-якого символу, зокрема і символу розриву рядка.

(?m)

Робить прив’язки ^ і $ відповідниками початку і кінця рядків, а не відповідниками початку або кінця усього тексту.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »

- Створення власних додатків для розширення функціональних можливостей calibre

Створення власних додатків для розширення функціональних можливостей calibre

У calibre дуже багато модулів. Майже усі функціональні можливості calibre реалізовано у формі додатків. Додатки використовуються для перетворення даних, для отримання новин (у формі так званих рецептів), для різноманітних компонентів інтерфейсу користувача, для встановлення з’єднання з різними пристроями, для обробки файлів під час додавання їх до бібліотеки calibre тощо. Із повним списком вбудованих додатків calibre можна ознайомитися на сторінці Налаштування  →  Додатково  →  Додатки.

У цьому розділи ми навчимо вас створювати власні додатки для додавання нових можливостей до calibre.

Зміст

- Анатомія додатка до calibre

- Додаток із інтерфейсом користувача

- __init__.py

- ui.py

- main.py

- Отримання ресурсів з файла ZIP додатка

- Уможливлення налаштовування вашого додатка користувачем

- Додатки редагування книг

- main.py

- Додавання перекладів вашого додатка

- Програмний інтерфейс для роботи з додатками

- Діагностування проблем у додатках

- Інші приклади додатків

- Оприлюднення ваших додатків для використання іншими користувачами

Примітка

Наведені тут дані стосуються Calibre версії 0.8.60 та новіших версій

Анатомія додатка до calibre

Додаток calibre є дуже простим: це файл ZIP, який містить певний код мовою Python та усі інші ресурси, зокрема файли зображень, які використовуються у додатку. Не будемо тягти кота за хвіст, і перейдемо до базового прикладу.

Припустімо у вашій системі встановлено програму calibre, якою ви користуєтеся для створення різноманітних документів у форматах EPUB і MOBI для себе. Вам потрібно, щоб в усіх файлах, які створено calibre, було зазначено видавця «Hello world». Розберемо, як це робиться. Створіть файл із назвою __init__.py (це спеціальна назва, нею слід завжди користуватися для основних файлів ваших додатків) і введіть до нього такий код мовою Python:

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

Ось і усе. Щоб додати цей код до calibre як додаток, просто віддайте вказані команди у теці, у якій ви створили __init__.py:

calibre-customize -b .

Примітка

У macOS засоби керування за допомогою командного рядка є частиною пакунка calibre. Наприклад, якщо calibre встановлено до /Applications, засоби керування за допомогою командного рядка зберігатимуться у каталозі /Applications/calibre.app/Contents/MacOS/.

Ви можете отримати додаток Hello World тут [https://calibre-ebook.com/downloads/helloworld_plugin.zip].

Кожного разу, коли ви користуватиметеся calibre для перетворення книги, програма викликатиме метод run() додатка, який встановлюватиме значення поля видавця «Hello World». Цей додаток є тривіальним. Перейдемо до розгляду складнішого прикладу, за допомогою якого можна додати компонент до інтерфейсу користувача програми.

Додаток із інтерфейсом користувача

Код цього додатка складатиметься із декількох файлів (так зроблено для кращого структурування коду). За його допомогою ви зможете ознайомитися з тим, як отримувати ресурси (зображення або файли даних) з файла ZIP додатка, як надати змогу користувачам налаштовувати ваш додаток, як створювати елементи інтерфейсу користувача calibre та як отримувати доступ до бази даних книг calibre і надсилати запити до цієї бази даних.

Ви можете отримати цей додаток тут [https://calibre-ebook.com/downloads/interface_demo_plugin.zip].

Першим, що слід зауважити, є те, що цей архів ZIP містить набагато більше файлів. Їхнє призначення описано нижче. Зверніть особливу увагу на файл plugin-import-name-interface_demo.txt.

plugin-import-name-interface_demo.txt

Порожній текстовий файл використовується для вмикання багатофайлової структури додатка. Цей файл має бути в усіх додатках, які складаються з декількох файлів .py. Він має бути порожнім, а його назва повинна мати таку формат: «plugin-import-name-якась_назва.txt» Наявність цього файла надає вам змогу імпортувати код з файлів .py у файлі ZIP за допомогою ось такої інструкції:

from calibre_plugins.some_name.some_module import some_object

Префікс calibre_plugins має бути завжди. some_name береться з назви порожнього текстового файла. some_module посилається на файл some_module.py у файлі ZIP. Зауважте, що це імпортування настільки ж потужне, наскільки є потужними імпортування у звичайному Python. Ви можете створювати пакунки і підпакунки модулів .py у файлі ZIP у звичайний спосіб (визначенням __init__.py у кожній підтеці), і все має просто працювати.

Назва, яку ви використали замість some_name, входить до загального простору назв, який є спільним для усіх додатків, тому вона має бути якомога неповторнішою. Втім, не слід забувати, що ця назва має бути коректним ідентифікатором Python (складатися лише з літер латинської абетки, цифр та символів підкреслювання).

__init__.py

Як і раніше, це файл, який визначає клас додатка

main.py

У цьому файлі міститься код, який виконує якусь корисну дію

ui.py

Цей файл визначає частину додатка, яка відповідає за інтерфейс

images/icon.png

Піктограма для додатка

about.txt

Текстовий файл з інформацією щодо додатка

translations

Тека, у якій містяться файли .mo з перекладами інтерфейсу користувача вашого додатка різними мовами. Докладніший опис наведено нижче.

Тепер зазирнемо до коду.

__init__.py

Спочатку, обов’язковий __init__.py для визначення метаданих додатка:

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

Єдиною вартою уваги особливістю є поле actual_plugin. Оскільки у calibre є одразу два інтерфейси, командний рядок і графічний інтерфейс, додатки з графічним інтерфейсом, як цей, не повинні завантажувати жодних бібліотек графічного інтерфейсу а __init__.py. За вас це робить поле actual_plugin, повідомляючи calibre, що сам код додатка можна знайти у іншому файлі у вашому архіві ZIP. Цей код буде завантажено лише у контексті графічного інтерфейсу.

Не забудьте, що для того, щоб це спрацювало, вам слід додати до вашого архіву ZIP додатка файл plugin-import-name-some_name.txt, про який ми писали вище.

Крім того, існує декілька методів для уможливлення налаштовування додатка користувачем. Їх опис наведено нижче.

ui.py

Тепер погляньмо на ui.py, де визначається сам код додатка з графічним інтерфейсом. Початковий код містить повний набір коментарів, отже вам буде доволі просто у ньому розібратися, якщо ви знаєте англійську:

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

Програмна логіка для реалізації діалогового вікна Interface Plugin Demo.

from qt.core import QDialog, QLabel, QMessageBox, QPushButton, QVBoxLayout class DemoDialog(QDialog):

def __init__(self, gui, icon, do_user_config):
QDialog.__init__(self, gui)
self.gui = gui self.do_user_config = do_user_config

# The current database shown in the GUI
# db is an instance of the class LibraryDatabase from db/legacy.py
# This class has many, many methods that allow you to do a lot of
# things. For most purposes you should use db.new_api, which has
# a much nicer interface from db/cache.py self.db = gui.current_db self.l = QVBoxLayout()
self.setLayout(self.l)

self.label = QLabel(prefs['hello_world_msg'])
self.l.addWidget(self.label)

self.setWindowTitle('Interface Plugin Demo')
self.setWindowIcon(icon)

self.about_button = QPushButton('About', self)
self.about_button.clicked.connect(self.about)
self.l.addWidget(self.about_button)

self.marked_button = QPushButton(
'Show books with only one format in the calibre GUI', self)
self.marked_button.clicked.connect(self.marked)
self.l.addWidget(self.marked_button)

self.view_button = QPushButton(
'View the most recently added book', self)
self.view_button.clicked.connect(self.view)
self.l.addWidget(self.view_button)

self.update_metadata_button = QPushButton(
'Update metadata in a book\'s files', self)
self.update_metadata_button.clicked.connect(self.update_metadata)
self.l.addWidget(self.update_metadata_button)

self.conf_button = QPushButton(
'Configure this plugin', self)
self.conf_button.clicked.connect(self.config)
self.l.addWidget(self.conf_button)

self.resize(self.sizeHint())

def about(self):
# Get the about text from a file inside the plugin zip file
# The get_resources function is a builtin function defined for all your
# plugin code. It loads files from the plugin zip file. It returns
# the bytes from the specified file.
#
# Note that if you are loading more than one file, for performance, you
# should pass a list of names to get_resources. In this case,
# get_resources will return a dictionary mapping names to bytes. Names that
# are not found in the zip file will not be in the returned dictionary.
text = get_resources('about.txt')
QMessageBox.about(self, 'About the Interface Plugin Demo', text.decode('utf-8'))

def marked(self):
''' Show books with only one format '''
db = self.db.new_api matched_ids = {book_id for book_id in db.all_book_ids() if len(db.formats(book_id)) == 1}
# Mark the records with the matching ids
# new_api does not know anything about marked books, so we use the full
# db object self.db.set_marked_ids(matched_ids)

# Tell the GUI to search for all marked records self.gui.search.setEditText('marked:true')
self.gui.search.do_search()

def view(self):
''' View the most recently added book '''
most_recent = most_recent_id = None db = self.db.new_api for book_id, timestamp in db.all_field_for('timestamp', db.all_book_ids()).items():
if most_recent is None or timestamp > most_recent:
most_recent = timestamp most_recent_id = book_id if most_recent_id is not None:
# Get a reference to the View plugin view_plugin = self.gui.iactions['View']
# Ask the view plugin to launch the viewer for row_number view_plugin._view_calibre_books([most_recent_id])

def update_metadata(self):
'''
Set the metadata in the files in the selected book's record to match the current metadata in the database.
'''
from calibre.ebooks.metadata.meta import set_metadata from calibre.gui2 import error_dialog, info_dialog

# Get currently selected books rows = self.gui.library_view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
return error_dialog(self.gui, 'Cannot update metadata',
'No books selected', show=True)
# Map the rows to book ids ids = list(map(self.gui.library_view.model().id, rows))
db = self.db.new_api for book_id in ids:
# Get the current metadata for this book from the db mi = db.get_metadata(book_id, get_cover=True, cover_as_data=True)
fmts = db.formats(book_id)
if not fmts:
continue for fmt in fmts:
fmt = fmt.lower()
# Get a python file object for the format. This will be either
# an in memory file or a temporary on disk file ffile = db.format(book_id, fmt, as_file=True)
ffile.seek(0)
# Set metadata in the format set_metadata(ffile, mi, fmt)
ffile.seek(0)
# Now replace the file in the calibre library with the updated
# file. We dont use add_format_with_hooks as the hooks were
# already run when the file was first added to calibre.
db.add_format(book_id, fmt, ffile, run_hooks=False)

info_dialog(self, 'Updated files',
'Updated the metadata in the files of %d book(s)'%len(ids), show=True)

def config(self):
self.do_user_config(parent=self)
# Apply the changes self.label.setText(prefs['hello_world_msg'])

Отримання ресурсів з файла ZIP додатка

Система завантаження додатків calibre визначає декілька вбудованих функцій, за допомогою яких зручно отримувати файли з архіву ZIP.

get_resources(назва_або_список_назв)

Цій функції слід передати список шляхів до файлів у архіві ZIP. Наприклад, щоб отримати доступ до файла icon.png у теці images архіву ZIP, вам слід скористатися таким шляхом: images/icon.png. Завжди використовуйте звичайну (не зворотну) похилу риску як роздільник шляхів, навіть у Windows. Якщо ви передасте функції одну назву, вона поверне необроблений байтовий вміст файла або None, якщо у архіві ZIP не буде знайдено відповідного файла. Якщо ви передасте декілька назв, функція поверне словник із прив’язкою назв до байтового вмісту. Якщо файла знайдено не буде, його не буде і у повернутому словнику.

get_icons(назва_або_список_назв, назва_додатка=““)

Обгортка до get_resources(), яка створює об’єкти QIcon з послідовності байтів, яку повернуто get_resources. Якщо у файлі ZIP не буде знайдено даних із відповідною назвою, відповідний об’єкт QIcon буде порожнім. З метою підтримки тем піктограм, передайте зручну для читання назву вашого додатка як параметр назва_додатка. Якщо користувач використовує тему піктограм для вашого додатка, ці піктограми матиму пріоритет при завантаженні.

Уможливлення налаштовування вашого додатка користувачем

Щоб надати користувачам змогу налаштовувати ваш додаток, вам слід визначити три методи у вашому основному класі додатка: is_customizable, config_widget і save_settings, ось так:

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 передбачено багато різних способів зберігання файлів налаштувань (спадок довгої історії програми). Рекомендованим способом є використання класу JSONConfig, який зберігає дані налаштувань у файлі .json.

Програмний код для керування даними налаштувань у демонстраційному додатку зберігається у 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 у коді додатка тепер можна здійснити простим:

from calibre_plugins.interface_demo.config import prefs

Ось як об’єкт prefs додається у main.py:

self.do_user_config(parent=self)
# Apply the changes
self.label.setText(prefs['hello_world_msg'])

Додатки редагування книг

Тепер, давайте трохи змінимо тему і поглянемо на те, як створити додаток для додавання інструментів до редактора книг calibre. Сам код додатка можна знайти :download_file: тут <editor_demo_plugin.zip>.

Першим кроком, як і для всіх інших додатків, є створення порожнього текстового файла із імпортованою назвою, як це описано вище. Назвемо цей файл plugin-import-name-editor_plugin_demo.txt.

Тепер створимо обов’язковий файл __init__.py, який міститиме метадані додатка — його назву, автора, дані щодо версії тощо.

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)

Один додаток редактора може містити декілька інструментів. Кожному інструменту відповідає одна кнопка на панелі інструментів і один запис у меню Додатки редактора. Ці пункти можуть мати підменю, якщо із інструментом пов’язано декілька дій.

Усі інструменти має бути визначено у файлі main.py вашого додатка. Кожен інструмент є класом, який успадковує властивості від класу calibre.gui2.tweak_book.plugin.Tool. Погляньмо на main.py з демонстраційного додатка. У коді багато коментарів, тому його призначення буде зрозумілим кожному, хто володіє англійською мовою. Докладніші відомості можна знайти у документації з програмного інтерфейсу, а саме класу calibre.gui2.tweak_book.plugin.Tool.

main.py

Тут ми бачимо визначення одного інструмента, який змінює розміри усіх шрифтів у книзі на коефіцієнт, вказаний користувачем. Цей інструмент демонструє різноманітні важливі концепції, які знадобляться вам для розробки власних додатків. Отже, вам варто уважно ознайомитися із початковим кодом (рясно перемежованим коментарями).

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

Давайте розберемо main.py. Як бачимо, тут визначено один інструмент із назвою Magnify fonts (Збільшити шрифт). Цей інструмент попросить користувача ввести число і помножить розміри усіх шрифтів у книзі на це число.

Першою важливою річчю є назва інструмента, яка має бути відносно унікальним рядком, яким можна скористатися як ключем до цього інструмента.

Іншою важливою вхідною точкою є метод calibre.gui2.tweak_book.plugin.Tool.create_action(). Цей метод створює об’єкти QAction, які з’являються на панелі інструментів додатків і у меню додатків. Він також, необов’язково, призначає клавіатурну комбінацію, яку зможе змінити користувач. Увімкнений сигнал від дії QAction з’єднується із методом ask_user(), який просить користувача ввести коефіцієнт збільшення шрифту, також запускає код збільшення.

Код збільшення добре прокоментовано, він є доволі простим. Основним, що слід зауважити, є те, що ви отримуєте посилання на вікно редактора як self.gui і Boss редактора як self.boss. Boss — об’єкт, який контролює інтерфейс користувача редактора. У коді міститься багато корисних методів, які документовано у класі calibre.gui2.tweak_book.boss.Boss.

Нарешті, у коді бачимо self.current_container, який є посиланням на книгу, яка редагується, як об’єкт calibre.ebooks.oeb.polish.container.Container. Він представляє книгу як збірку його складових файлів HTML, CSS, зображень та містить зручні методи для виконання багатьох корисних речей. Об’єкт контейнера та різні корисні допоміжні функції, якими ви можете скористатися у коді вашого власного додатка, документовано у розділі Документація з програмного інтерфейсу засобів редагування електронних книг.

Додавання перекладів вашого додатка

Ви можете зробити усі рядки у інтерфейсі вашого додатка придатними для перекладу з можливістю показу будь-якою мовою, якою перекладено основний інтерфейс користувача calibre.

Спочатку слід пройтися програмним кодом вашого додатка і позначити усі видимі для користувача рядки як придатні для перекладу за допомогою обгортання їх конструкцією _(). Приклад:

action_spec = (_('My plugin'), None, _('My plugin is cool'), None)

Далі, скористайтеся якоюсь програмою для створення файлів .po на основі початкового коду вашого додатка. Для кожної мови, якою ви перекладатимете програму, має бути окремий файл .po. Приклади: de.po для німецької, fr.po для французької тощо. Для виконання цієї дії ви можете скористатися програмою poedit [https://poedit.net/].

Надішліть ці файли .po вашим перекладачам. Отримавши від них переклади, зберіть з них файли .mo. Для цього ви знову можете скористатися poedit або віддати команду:

calibre-debug -c "from calibre.translations.msgfmt import main; main()" filename.po

Розташуйте файли .mo у теці «translations» дерева коду вашого додатка.

Останнім кроком буде простий виклик функції load_translations() у початковій частині файлів .py вашого додатка. З міркувань швидкодії вам варто викликати цю функцію лише у тих файлах .py, які насправді містять рядки для перекладу. Отже, у типовому додатку з інтерфейсом для користувача вам слід викликати цю функцію на початку ui.py, а не на початку __init__.py.

Ви можете перевірити переклад ваших додатків, змінивши мову інтерфейсу користувача у calibre за допомогою сторінки Налаштування  →  Інтерфейс  →  Зовнішній вигляд або запустивши calibre зі встановленою змінною середовища CALIBRE_OVERRIDE_LANG . Приклад:

CALIBRE_OVERRIDE_LANG=de

Замініть «de» на код мови, яку ви хочете тестувати.

Для перекладу із формами множини скористайтеся функцією ngettext() замість _(). Приклад:

ngettext('Delete a book', 'Delete {} books', num_books).format(num_books)

Програмний інтерфейс для роботи з додатками

Як ви вже могли помітити раніше, додаток у calibre є класом. У calibre різним класам відповідають різні типи додатків. Подробиці щодо усіх класів, зокрема назви базових класів усіх додатків, наведено у розділі Документація з програмного інтерфейсу для роботи з додатками.

Ваш додаток, майже напевне, використовуватиме код з calibre. Щоб навчитися шукати потрібні вам частини у програмному коді calibre, ознайомтеся із розділом Структура коду.

Діагностування проблем у додатках

Першим, найважливішим кроком є запуск calibre у режимі діагностики. Зробити це можна за допомогою такої команди, відданої з командного рядка:

calibre-debug -g

Або з вікна calibre: натиснути стрілку праворуч від кнопки Налаштування або скористатися комбінацією клавіш Ctrl+Shift+R.

Якщо додаток запущено з командного рядка, діагностичні повідомлення виводитимуться до консолі. Якщо ж запуск виконувався з calibre, виведені дані буде записано до текстового файла.

Ви можете вставити інструкції виведення даних (print) будь-де у вашому коді додатка. Ці інструкції виводитимуть дані у режимі діагностики. Не забувайте, це Python, отже для діагностики вам не потрібно нічого окрім інструкцій print ;) Усі додатки, створені автором calibre, було написано з використанням саме цієї методики діагностики.

Ви можете швидко виконати тестування змін у вашому додатку за допомогою такої команди:

calibre-debug -s; calibre-customize -b /path/to/your/plugin/folder; calibre

Ця команда завершить роботу запущеної програми calibre, зачекає на завершення роботи, далі оновить ваш додаток у calibre і перезапустить calibre.

Інші приклади додатків

Список багатьох складних додатків до calibre можна знайти тут [https://www.mobileread.com/forums/showthread.php?t=118764].

Оприлюднення ваших додатків для використання іншими користувачами

Якщо ви хочете поділитися створеними вами додатками з іншими користувачами програми, створіть новий допису на форумі додатків calibre [https://www.mobileread.com/forums/forumdisplay.php?f=237] і долучіть до нього ваш додаток.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Формули в електронних книгах

Формули в електронних книгах

У програмі для перегляду електронних книг calibre передбачено можливість показу вбудованих до електронної книги формул (у файлах ePub та HTML). Формули може бути набрано безпосередньо у TeX, MathML або AsciiMath. Для показу формул у програмі для перегляду книг calibre використовується чудова бібліотека MathJax [https://www.mathjax.org]. За допомогою цього короткого підручника ви зможете навчитися створенню електронних книг із формулами, які згодом можна буде переглядати у програмі для перегляду електронних книг calibre.

Простий файл HTML із формулами

Ви можете вписати формулу до простого файла HTML, і програма для перегляду книг calibre перетворить її на чудову типографіську композицію. У наведеному нижче прикладі ми скористаємося форматом TeX. Ви зможете переконатися, що можливе використання усіх звичайних команд TeX, за винятком амперсандів та символів «менше» та «більше», які слід записувати у позначеннях HTML: &amp; &lt; та &gt;, відповідно.

Спочатку слід повідомити calibre, що документ містить формули. Зробити це можна додаванням такого фрагмента коду до розділу <head> файла HTML:

<script type="text/x-mathjax-config"></script>

Ось так, тепер можна вставляти формули у той самий спосіб, у який ви вставляєте їх у файлі .tex. Наприклад, ось так можна вставити рівняння Лоренца:

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

У програмі для перегляду книг calibre цей фрагмент буде показано так, як на наведеному нижче знімку.

Рівняння Лоренца

Код файла HTML з іншими рівняннями та вбудованими формулами повністю наведено нижче. Ви можете перетворити цей файл HTML на файл формату EPUB за допомогою calibre, щоб отримати електронну книгу, якою буде просто поділитися з іншими людьми.

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

Додаткові відомості

Оскільки для показу формул у calibre використовується бібліотека MathJax, найкращим місцем для отримання додаткових відомостей щодо формул у електронних книгах та довідкової інформації є сайт MathJax [https://www.mathjax.org].

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Створення каталогів AZW3 • EPUB • MOBI

Створення каталогів AZW3 • EPUB • MOBI

За допомогою можливості створення каталогів у calibre ви зможете створювати каталоги вашої бібліотеки у різноманітних форматах. У цьому файлі довідки описано параметри створення каталогів у форматах AZW3, EPUB та MOBI.

- Вибір книг каталогу

- Включені розділи

- Префікси

- Виключені книги

- Виключені жанри

- Інші параметри

- Нетипові обкладинки каталогів

- Додаткові інформаційні ресурси

Вибір книг каталогу

Якщо ви хочете каталогізувати усю вашу бібліотеку, зніміть будь-які критерії пошуку або фільтрування у основному вікні програми. Якщо позначено одну книгу, кандидатами на включення до вашого каталогу будуть усі книги у бібліотеці. Окремі книги може бути виключено за різними критеріями. Докладніший опис можна знайти у розділі Виключені жанри.

Якщо вам потрібен каталог частини вашої бібліотеки, ви можете скористатися одним з двох варіантів:

- Створіть позначений набір книг, які ви хочете каталогізувати. Якщо у головному вікні calibre позначено декілька книг, каталогізовано буде лише позначені книги.

- Скористайтеся полем пошуку або панеллю перегляду міток для фільтрування списку показаних книг. Каталогізовано буде лише показані книги.

Щоб розпочати створення каталогу, виберіть пункт меню Перетворити книги  →  Створити каталог книг у вашій бібліотеці calibre. Ви також можете додати на панель інструментів Створити каталог за допомогою сторінки Налаштування  →  Інтерфейс  →  Панелі інструментів і меню, щоб вам було простіше дістатися діалогового вікна створення каталогу.

У Параметрах каталогу виберіть як формат каталогу AZW3, EPUB або MOBI. У полі Заголовок каталогу вкажіть назву, яку буде використано для створеного каталогу. Якщо каталог із такою назвою і у такому форматі вже існує, його буде замінено новоствореним каталогом.

Позначення пункту Надсилати каталог на пристрій автоматично призведе до вивантаження створеного каталогу на з’єднаний з комп’ютером пристрій одразу після завершення обробки.

Включені розділи

Розділи, які ви позначите, буде включено до створеного каталогу:

- Автори — усі книги, упорядковані за авторами у форматі списку. Книги поза циклами у списку перебуватимуть вище за книги з циклів.

- Заголовки — усі книги, упорядковані за назвами, представлені у форматі списку.

- Цикл — усі книги, які є частиною циклів, упорядковані за циклами, представлені у форматі списку.

- Жанри — окремі жанри у форматі списку, упорядкованому за авторами і циклами.

- Нещодавно додані — усі книги, упорядковані у зворотному хронологічному порядку. До списку буде включено книги, які було додано протягом останніх 30 днів, далі, список доданих книг за місяцями.

- Описи — докладна сторінка опису для кожної книги, включно з мініатюрою обкладинки та коментарями. Упорядковується за авторами, книги поза циклами буде показано у списку перед книгами з циклів.

Префікси

Правила префіксів надають вам змогу додавати префікс до списків книг, якщо виконуються певні критерії. Наприклад, можна позначити книги, які ви прочитали або які є у вашому списку книг, які ви хочете прочитати, літерою X.

Поле для позначки у першому стовпчику призначено для вмикання правила. Назва — назва правила. Поле — Мітки або нетиповий стовпчик з вашої бібліотеки. Значення — вміст Поля, відповідність якого слід встановити. Якщо виконується правило префікса, книгу буде позначено вибраним Префіксом.

У наведеному вище прикладі вказано три правила префіксів:

1. Прочитана книга вказує, що до запису книги, у яких є будь-яка дата у нетиповому стовпчику із назвою Останнє читання, буде позначено префіксом із символом позначення.

2. Запис Бажана книга визначає, що до запису будь-якої книги із міткою Бажана книга буде дописано префікс X.

3. Бібліотека визначає, що будь-яка книга зі значенням True (або Yes) у нетиповому стовпчику Available in Library, отримає префікс — символ подвійної стрілки.

Перше відповідне правило визначає префікс. Вимкнені або неповні правила ігноруються.

Виключені книги

Правила виключення надають вам змогу вказати книги, які не буде каталогізовано.

Поле для позначки у першому стовпчику призначено для вмикання правила. Назва — назва правила. Поле — Мітки або нетиповий стовпчик з вашої бібліотеки. Значення — вміст Поля, відповідність якого слід встановити. Якщо виконується правило виключення, книгу буде виключено зі створеного каталогу.

У наведеному вище прикладі було вказано два правила виключення:

1. Правило Каталоги визначає, що будь-яку книгу із міткою Каталог буде виключено зі створеного каталогу.

2. Правило Архівовані книги визначає, що будь-яку книгу із міткою Архівовано у нетиповому стовпчику Стан буде виключено зі створеного каталогу.

Усі правила буде застосовано до кожної книги. Вимкнені або неповні правила буде проігноровано.

Виключені жанри

Під час створення каталогу мітки у базі даних буде використано як жанри. Наприклад, ви можете використати мітки Художня література і Документальна література. Ці мітки стануть у створеному каталозі жанрами. Книги потраплять до списків відповідних жанрів на основі призначених міток. Запис книги буде показано у списках кожного жанру, мітку якого пов’язано із книгою.

Можливо, ви використовуєте мітки з певною метою, наприклад, мітку «+» для позначення прочитаних книг або мітку у квадратних дужках, зокрема [Amazon Freebie], для позначення джерела книги. За допомогою формального виразу Виключені жанри ви можете вказати мітки, які не слід використовувати як жанри у створеному каталозі. Типовим формальним виразом взірця для виключення є \[.+\]\+. За допомогою цього взірця можна виключити книги, які мають будь-які мітки у формі [мітка], а також виключити книги з міткою +, типовою міткою для прочитаних книг, зі створеного каталогу.

Ви також можете використовувати точну назву мітки у формальному виразі. Наприклад, [Amazon Freebie] або [Project Gutenberg]. Якщо ви хочете вказати список декількох точних міток для виключення, додайте символ вертикальної риски між мітками: [Amazon Freebie]|[Project Gutenberg].

У списку Результати формального виразу буде показано, які мітки буде виключено під час побудови каталогу на основі міток у базі даних та введеного вами взірця формального виразу. Список результатів автоматично оновлюватиметься у відповідь на внесення змін до формального виразу взірця.

Інші параметри

Обкладинка каталогу визначає, слід створювати обкладинку каталогу чи скористатися наявним зображенням обкладинки. Передбачено можливість створення нетипового зображення обкладинки для ваших каталогів — див. Нетипові обкладинки каталогів, щоб дізнатися більше. Якщо вами створено нетипове зображення обкладинки, яким ви хочете повторно скористатися, виберіть Використати вже створену обкладинку. Якщо ж це не так, виберіть Створити обкладинку.

Додаткова нотатка опису визначає вміст нетипового стовпчика, який буде вставлено до сторінки опису поряд із мініатюрою зображення обкладинки. Наприклад, ви можете захотіти бачити дату, коли ви востаннє читали книгу, з використанням нетипового стовпчика Останнє читання. Щодо інших можливостей використання можливості додаткової нотатки опису, інформацію можна отримати з цього допису на форумі calibre [https://www.mobileread.com/forums/showpost.php?p=1335767&postcount=395].

Ширина мініатюри визначає бажану ширину мініатюр зображень обкладинок, які буде включено зі сторінками описів. Мініатюри кешуються для збільшення швидкодії. Щоб поекспериментувати із різними ширинами, спробуйте створити каталог із декількома книгами, а потім визначте на його прикладі бажану ширину, а потім вже створіть повний каталог. Під час початкового створення каталогу із новою шириною мініатюр програма може уповільнитися, але під час наступних спроб створення каталогів працюватиме набагато швидше, оскільки використовуватиме кеш мініатюр зображень обкладинок.

Об’єднати з коментарями вказує нетиповий стовпчик, вміст якого без пошкоджень під час створення каталогу буде об’єднано із метаданими коментарів. Наприклад, у вас міг бути нетиповий стовпчик Біографія автора, який би ви хотіли дописати до метаданих коментарів. Ви можете наказати програмі вставити вміст такого нетипового стовпчика до або після розділу «Коментарі» і, якщо хочете, відокремити дописані дані горизонтальною рискою. Серед прийнятних типів нетипових стовпчиків є text, comments, and composite.

Нетипові обкладинки каталогів

Якщо встановлено додаток створення обкладинок [https://www.mobileread.com/forums/showthread.php?t=124219], ви можете створювати нетипові обкладинки для вашого каталогу. Щоб встановити цей додаток, скористайтеся пунктом меню Налаштування  →  Додатково  →  Розширення  →  Отримати нові додатки.

Додаткові інформаційні ресурси

Щоб дізнатися більше про можливості каталогів у calibre, ознайомтеся із вмістом розділу форуму MobileRead Creating Catalogs - Start here [https://www.mobileread.com/forums/showthread.php?t=118556], де наведено дані щодо налаштовування шаблонів каталогів та того, які надсилати звіти про вади щодо реалізації каталогів.

Якщо ви хочете задати питання щодо можливості створення каталогів у calibre, відвідайте форум Library Management [https://www.mobileread.com/forums/forumdisplay.php?f=236].

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Навчальні посібники »
- Віртуальні бібліотеки

Віртуальні бібліотеки

У calibre віртуальна бібліотека — це спосіб повідомити calibre, що слід відкрити лише певну підмножину книг звичайної бібліотеки. Наприклад, ви можете наказати програмі працювати лише з книгами певного автора або книгами, які мають певну мітку. Використання віртуальних бібліотек є пріоритетним способом поділу вашої великої збірки книг на менші часткові збірки. Цей спосіб краще поділу вашої бібліотеки на менші, оскільки якщо ви захочете виконати пошук у всій збірці, вам достатньо буде повернутися до повної бібліотеки. У calibre не передбачено способу одночасного пошуку у декількох окремих бібліотеках.

Віртуальна бібліотека відрізняється від простого результату пошуку. Пошук обмежує вміст списку книг, який буде показано у основному вікні програми. Віртуальна бібліотека робить те саме, але, крім того, також обмежує перелік записів, які буде показано на панелі Перегляд міток у лівій частині вікна програми. На панелі перегляду міток буде показано лише ті мітки, авторів, цикли, видавців тощо, які мають книги з віртуальної бібліотеки. Таким чином, віртуальна бібліотека поводить себе так само, як звичайна бібліотека, але містить лише обмежений набір книг з основної бібліотеки.

Створення віртуальних бібліотек

Щоб скористатися віртуальною бібліотекою, натисніть кнопку Віртуальна бібліотека, розташовану ліворуч від панелі пошуку, і виберіть пункт Створити віртуальну бібліотеку. Наприклад, створімо віртуальну бібліотеку, у якій будуть лише книги певного автора. Для цього слід натиснути посилання Автори, як це показано на наведеному нижче зображенні, вибрати певного автора і натиснути кнопку «Гаразд».

Програма заповнить відповідними даними діалогове вікно створення віртуальної бібліотеки. Натисніть кнопку «Гаразд», і ви побачите створену нову віртуальну бібліотеку. Програма автоматично перемкнеться на її показу. Буде показано лише книги, автором яких є вибраний вами автор. З точки зору calibre виникне бібліотека, у якій міститимуться лише книги вибраного автора.

Ви можете будь-коли повернутися до режиму показу усієї бібліотеки натисканням кнопки Віртуальна бібліотека з наступним вибором пункту із назвою <Немає>.

Віртуальні бібліотеки засновано на пошуках. Ви можете скористатися результатами будь-якого пошуку для створення віртуальної бібліотеки. Така віртуальна бібліотека міститиме лише книги, які відповідають критеріям пошуку. Для створення такої бібліотеки слід спочатку ввести критерій пошуку на панелі пошуку або побудувати пошук за допомогою панелі Перегляд міток. Коли буде знайдено усі потрібні вам книги, натисніть кнопку Віртуальна бібліотека, виберіть пункт Створити бібліотеку і введіть назву нової віртуальної бібліотеки. Програма створить віртуальну бібліотеку на основі вказаних вами критеріїв пошуку. Пошук є дуже потужним інструментом. Приклади речей, які можна робити за його допомогою наведено у розділі Інтерфейс пошуку.

Корисні приклади віртуальних бібліотек

- Книги, які додано до calibre минулого дня:

date:>1daysago

- Книги, які додано до calibre минулого місяця:

date:>30daysago

- Книги з оцінкою 5 зірок:

rating:5

- Книги з оцінкою у принаймні 4 зірки:

rating:>=4

- Книги без оцінки:

rating:false

- Періодика завантажена за допомогою функції «Отримати новини»:

tags:=News and author:=calibre

- Книги без міток:

tags:false

- Книги без обкладинок:

cover:false

Робота з віртуальними бібліотеками

Ви можете редагувати раніше створену віртуальну бібліотеку або вилучити її. Для цього натисніть пункт Віртуальна бібліотека і виберіть відповідний пункт дії.

Ви можете повідомити calibre, що хочете завжди застосовувати певну віртуальну бібліотеку після відкриття поточної бібліотеки. Для цього скористайтеся сторінкою Налаштування  →  Інтерфейс  →  Поведінка.

Ви можете скористатися поточними критеріями пошуку для створення тимчасової віртуально бібліотеки. Для цього натисніть кнопку Віртуальна бібліотека і виберіть у меню пункт *поточний пошук.

Ви можете переглядати усі доступні віртуальні бібліотеки як вкладки над списком книг. Це, зокрема, зручно, якщо ви часто перемикаєтеся між різними віртуальними бібліотеками. Щоб увімкнути цей режим перегляду, натисніть кнопку Віртуальна бібліотека і виберіть у меню пункт Показувати віртуальні бібліотеки як вкладки. Ви можете перевпорядкувати вкладки перетягуванням зі скиданням або закрити непотрібні, ті, які ви не хочете бачити. Закриті вкладки можна знову відкрити, вибравши відповідний пункт із контекстного меню (викликається клацанням правою кнопкою миші) панелі вкладок.

Використання у пошуках віртуальних бібліотек

Пошук книги у віртуальній бібліотеці можна виконати за допомогою префікса «vl:». Наприклад, за критерієм «vl:Прочитане» буде знайдено усі книги у віртуальній бібліотеці «Прочитане». За критерієм пошуку «vl:Прочитане and vl:»Наукова фантастика»» буде знайдено усі книги, які належать одночасно до віртуальних бібліотек «Прочитане» і «Наукова фантастика».

Значення після префікса «vl:» має бути назвою віртуальної бібліотеки. Якщо у назві містяться пробіли, слід взяти її у лапки.

Одним із випадків використання пошуку у віртуальній бібліотеці є використання сервера даних. За допомогою поля Налаштування  →  Обмін мережею  →  Вимагати ім’я користувача і пароль ви можете обмежити видимість бібліотек calibre для користувача. Для кожної віртуальної бібліотеки ви можете вказати вираз для пошуку для подальшого обмеження показаних книг. Скористайтеся виразом «vl:»Назва віртуальної бібліотеки»» для обмеження переліку книг лише тими, які є у віртуальній бібліотеці.

Використання додаткових обмежень

Ще більше обмежити список книг, показаних у віртуальній бібліотеці, можна за допомогою пункту Додаткові обмеження. Додатковим обмеженням є збережений вами пошук, який можна застосувати до поточної віртуальної бібліотеки з метою подальшого обмеження списку показаних у ній книг. Наприклад, нехай ви маєте віртуальну бібліотеку для книг з міткою Історична белетристика і збережений пошук, який показує вам непрочитані книги. Тоді, ви можете натиснути кнопку Віртуальна бібліотека і вибрати пункт Додаткове обмеження, щоб програма показала вам лише непрочитану історичну белетристику. Щоб дізнатися більше про збережені пошуки, див. розділ Збереження результатів пошуку.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Схема адрес calibre://

Схема адрес calibre://
calibre реєструє себе як програму-обробник для адрес calibre://. Ви можете скористатися такими адресами для виконання дій, зокрема відкриття книг, пошуку книг тощо, з інших програм або документів або за допомогою командного рядка. Наприклад, ось така команда з командного рядка:

calibre calibre://switch-library/Some_Library

відкриє вікно calibre із бібліотекою з назвою Some Library. Назви бібліотек збігаються із назвами тек відповідних бібліотек, але пробіли слід замінити на символи підкреслювання. Особливе значення — _ — вказує на поточну бібліотеку. Документацію щодо типів адрес наведено нижче.

Ви навіть можете вставляти такі посилання до файлів HTML або документів Word чи подібних документів, і операційна система автоматично запускатиме calibre для виконання вказаної вами дії.

- Перемкнутися на вказану бібліотеку

- Показати вказану книгу у calibre

- Відкрити вказану книгу у засобі перегляду електронних книг на вказаній позиції

- Пошук книг

- Відкрити вікно подробиць щодо книги для певної книги у деякій бібліотеці

- Open the notes associated with an author/series/etc.

- Шістнадцяткове кодування параметрів адреси

Перемкнутися на вказану бібліотеку

Синтаксис адреси:

calibre://switch-library/Library_Name

Назви бібліотек збігаються із назвами тек відповідних бібліотек, але пробіли слід замінити на символи підкреслювання. Особливе значення — _ — вказує на поточну бібліотеку. Ви також можете скористатися шістнадцятковим кодуванням назв бібліотек. Таке кодування є корисним для назв бібліотек, які містять спеціальні символи, які потребують кодування у звичайних інтернет-адресах. Назви бібліотек у шістнадцятковому кодуванні виглядають так:

_hex_-AD23F4BC

де частина після префікса _hex_- є назвою бібліотеки у кодуванні UTF-8 — кожному байту відповідає два шістнадцяткових символи.

Показати вказану книгу у calibre

Синтаксис адреси:

calibre://show-book/Library_Name/book_id

Ця команда покаже книгу із ідентифікатором book_id (числовим ідентифікатором) у calibre. Ідентифікатор книги можна переглянути у графічному інтерфейсі calibre — достатньо навести вказівник миші на посилання Натисніть, щоб відкрити на Панелі подробиць щодо книги. Число у квадратних дужках наприкінці шляху до теки бібліотеки книги і є числовим ідентифікатором.

Ви можете скопіювати посилання на поточну показану у вікні calibre книгу. Для цього клацніть правою кнопкою на Панелі подробиць щодо книги і виберіть у контекстному меню пункт Копіювати посилання на книгу.

Якщо активним є пошук і книга не відповідає критерію пошуку, результати пошуку не буде очищено.

Якщо позначено віртуальну бібліотеку, calibre використає її при показі книги. Якщо книгу не буде знайдено у цій віртуальній бібліотеці, віртуальну бібліотеку очищено.

Якщо ви хочете перемкнутися на певну віртуальну бібліотеку при показі книги, скористайтеся таким:

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

замінює пробіли у назві віртуальної бібліотеки на %20. Якщо книги немає у цій віртуальній бібліотеці, віртуальну бібліотеку буде проігноровано.

Відкрити вказану книгу у засобі перегляду електронних книг на вказаній позиції

Синтаксис адреси:

calibre://view-book/Library_Name/book_id/book_format?open_at=location

Тут book_format є форматом книги, наприклад, EPUB або MOBI, а location — необов’язковий аргумент місця у книзі. Найпростішим способом отримання таких посилань є відкриття книги у засобі перегляду. Далі слід вибрати серед засобів керування переглядом Перейти → Місце — там буде показано посилання, яке ви зможете скопіювати і вставити у вікні іншої програми.

Пошук книг

Синтаксис адреси:

calibre://search/Library_Name?q=query
calibre://search/Library_Name?eq=hex_encoded_query

Тут query є коректним виразом для пошуку. Якщо вираз для пошук є складним, закодуйте його у шістнадцятковий рядок і скористайтеся замість нього eq. Якщо не вказувати запит для пошуку, поточне фільтрування пошуку у вікні програми буде знято.

Типово, якщо позначено віртуальну бібліотеку, calibre очистить її до виконання пошуку, щоб було знайдено усі книги. Якщо ви хочете зберегти віртуальну бібліотеку, скористайтеся такою командою:

calibre://search/Library_Name?q=query&virtual_library=_

Якщо ви хочете перемкнутися на певну віртуальну бібліотеку, скористайтеся такою командою:

calibre://search/Library_Name?virtual_library=Library%20Name
or
calibre://search/Library_Name?encoded_virtual_library=hex_encoded_virtual_library_name

замінивши пробіли у назві віртуальної бібліотеки рядками %20.

Якщо ви виконуєте пошук у calibre і хочете створити посилання на пошук, клацніть правою кнопкою на панелі пошуку і виберіть у контекстному меню пункт Копіювати пошук як адресу.

Відкрити вікно подробиць щодо книги для певної книги у деякій бібліотеці

Синтаксис адреси:

calibre://book-details/Library_Name/book_id

Відкриває вікно подробиць щодо книги на вказаній книзі з вказаної бібліотеки без зміни поточної бібліотеки або позначеної книги.

Open the notes associated with an author/series/etc.

Синтаксис адреси:

calibre://book-details/Library_Name/Field_Name/id_Item_Id

This opens a window showing the notes of the specified item.
The easiest way to create such URLs is to show the notes you want in calibre and click the Copy URL button to copy the URL to the clipboard and paste it wherever you need.

Here Field_Name is the name of the columns such as authors or tags.
For user created columns, replace the leading # in the field name with an underscore, so #mytags becomes _mytags.

In addition to specifying items by id using Item_Id you can also specify them by name using either val_Item_Name or hex_Hex_Encoded_Item_Name.
For example:

calibre://book-details/Library_Name/authors/val_John%20Doe

Шістнадцяткове кодування параметрів адреси

Шістнадцяткове кодування параметрів адреси виконується так: спочатку параметр кодують у байти UTF-8, а потім замінюють кожен байт на два шістнадцяткових символи, які відповідають байту. Наприклад, рядок abc складається з байтів 0x61 0x62 і 0x63 у UTF-8, тому кодованою версією рядка є такий рядок: 616263.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Налаштування calibre

Налаштування calibre

Програму calibre створено таким чином, щоб переносити значну частину можливостей у модулі. Ви можете змінювати режим роботи різних частин комплекту програми. За допомогою цього розділу ви навчитеся:

- користуванню змінними середовища та *коригуваннями» для налаштовування поведінки calibre;

- визначенню власних статичних ресурсів, зокрема піктограм та шаблонів, для перевизначення типових;

- використанню додатків для розширення функціональних можливостей calibre;

- оприлюдненню тем піктограм та додатків для інших користувачів calibre;

- щоб дізнатися більше про створення рецептів, які надають calibre доступ до нових джерел даних, скористайтеся розділом Додавання вашого улюбленого сайта новин.

Примітка

Зауважте, що хоча теми піктограм і додатки індексує вбудованим засобом оновлення calibre і можуть бути отримані за його допомогою, вони не є частиною calibre, а типовим місцем, де можна отримати підтримку та початковий код, є форум Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166] з відповідними гілками обговорення.

- Змінні середовища

- Коригування

- Перевизначення піктограм, шаблонів тощо

- Створення власної теми піктограм для calibre

- Налаштовування calibre за допомогою додатків

Змінні середовища

- CALIBRE_CONFIG_DIRECTORY — визначає теку, у якій будуть зберігатися файли налаштувань і з якої програма читатиме ці файли.

- CALIBRE_TEMP_DIR — визначає теку тимчасових даних, яка використовуватиметься calibre

- CALIBRE_CACHE_DIRECTORY — визначає теку, яку calibre використовуватиме для кешування даних між сеансами

- CALIBRE_OVERRIDE_DATABASE_PATH — надає вам змогу вказати шлях до metadata.db повністю. За допомогою цієї змінної ви можете зберігати metadata.db у каталозі, відмінному від каталогу бібліотеки. Корисно, якщо ваша тека бібліотеки зберігається на диску у мережі, для якого не передбачено підтримки блокування файлів.

- CALIBRE_DEVELOP_FROM — використовується для запуску із середовища розробки calibre. Див. Налаштовування середовища для розробки calibre.

- CALIBRE_OVERRIDE_LANG — використовується для примусового визначення мови інтерфейсу програми (у форматі коду мови ISO 639)

- CALIBRE_TEST_TRANSLATION — використовується для тестування файла перекладу .po (має містити шлях до файла .po)

- CALIBRE_NO_NATIVE_FILEDIALOGS — наказує calibre не використовувати власні діалогові вікна операційної системи для вибору файлів або тек.

- CALIBRE_NO_NATIVE_MENUBAR — наказує calibre не створювати природного (загального) меню в Ubuntu Unity та подібних стільничних середовищ у Linux. Замість цього, меню, традиційно, розташовуватиметься у верхній частині вікна програми.

- CALIBRE_USE_SYSTEM_THEME — типово, у Linux calibre використовує власний вбудований стиль Qt. Так зроблено, щоб уникнути аварійних завершень роботи і зависань, які спричинено несумісностями з версією Qt, за допомогою якої зібрано calibre, і версією Qt, яку встановлено у системі. Небажаним наслідком такого підходу є те, що calibre може не використовувати загальносистемні параметри вигляду та поведінки. Якщо ви встановите цю змінну середовища у Linux, calibre використовуватиме загальносистемні параметри теми. Це може призвести, як ми вже зауважували, до аварійних завершень роботи програми та повисань.

- CALIBRE_SHOW_DEPRECATION_WARNINGS — наказує calibre виводити попередження щодо застарілості певних функцій до стандартного виведення (stdout). Корисно для розробників calibre.

- CALIBRE_NO_DEFAULT_PROGRAMS — забороняє calibre автоматично реєструвати типи файлів, які може обробляти програма, у Windows.

- QT_QPA_PLATFORM — у Linux встановіть для змінної значення wayland, щоб наказати calibre використовувати Wayland, і xcb, щоб наказати програмі використовувати X11.

- SYSFS_PATH — використовується, якщо sysfs змонтовано кудись, окрім /sys

- http_proxy, https_proxy — використовуються у Linux для визначення проксі-сервера HTTP(S)

Див. Як налаштувати змінні середовища у Windows [https://www.computerhope.com/issues/ch000549.htm]. Якщо ви працюєте у macOS, ви можете налаштувати змінні середовища за допомогою створення файла ~/Library/Preferences/calibre/macos-env.txt, до якого слід вписати змінні середовища, по одній на рядок. Приклад:

CALIBRE_DEVELOP_FROM=$HOME/calibre-src/src
CALIBRE_NO_NATIVE_FILEDIALOGS=1
CALIBRE_CONFIG_DIRECTORY=~/.config/calibre

Коригування

Коригування — незначні зміни, якими можна керувати різними аспектами роботи calibre. Ви можете внести їх за допомогою пункту меню Налаштування -> Додатково -> Коригування. Типові значення коригувань наведено нижче

#!/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'

Перевизначення піктограм, шаблонів тощо

Примітка

У calibre передбачено безпосередню підтримку тем піктограм. У типовому пакунку програми є декілька таких тем. Змінити тему можна за допомогою пункту Налаштування  →  Інтерфейс  →  Зовнішній вигляд  →  Змінити тему піктограм. Перевагу слід надавати зміні тем піктограм, а не перевизначенню окремих піктограм.

У calibre передбачено можливість перевизначити статичні ресурси програми, зокрема піктограми, скрипти JavaScript та шаблони для панелі метаданих, каталогів тощо, скориставшись налаштованими вами відповідним чином версіями. Усі статичні ресурси зберігаються у підтеці resources теки, до якої було встановлено calibre. У Windows це, зазвичай, C:\Program Files\Calibre2\app\resources. У macOS це /Applications/calibre.app/Contents/Resources/resources/. У Linux, якщо ви використовуєте бінарний пакунок з сайта calibre, це /opt/calibre/resources. Шлях у вашій системі, звичайно ж, залежатиме від каталогу до якого ви встановили calibre.

Вам не варто безпосередньо змінювати файли у теці ресурсів, оскільки внесені вами зміни буде перезаписано новими версіями файлів під час оновлення calibre. Замість цього, відкрийте сторінку Налаштування  →  Додатково  →  Різне і натисніть кнопку Відкрити теку налаштувань calibre. У теці налаштувань створіть підтеку із назвою «resources» і розташуйте у ній файли, які слід перевизначити у типовому наборі. Файли слід розташовувати у відповідних підтеках. Наприклад, зображення мають зберігатися у resources/images тощо. calibre автоматично надаватиме пріоритет вашому нетиповому файлу перед вбудованим до пакунка під час наступного завантаження програми.

Наприклад, якщо ви хочете змінити піктограму для кнопки Вилучити книги, вам слід спочатку зазирнути до теки resources пакунка і знайти відповідний файл — resources/images/remove_books.png. Нехай тепер у вас є альтернативний варіант піктограми у форматі PNG із назвою my_remove_books.png. Його слід зберегти до теки налаштувань як resources/images/remove_books.png. Усі піктограми, які calibre використовує у інтерфейсі користувача, зберігаються у каталозі resources/images та його підкаталогах. Розташування перевизначеного файла у цьому каталозі матиме вищий пріоритет навіть за використання нетипової теми піктограм.

Створення власної теми піктограм для calibre

Якщо вами було створено красивий набір піктограм, і ви хочете поділитися ним із іншими користувачами calibre, додавши його до набору вбудованих тем піктограм calibre, ви можете запакувати ваші піктограми у власну тему. Для цього відкрийте сторінку Налаштування  →  Різне  →  Створити тему піктограм, виберіть теку, у якій ви зберегли ваші піктограми. Далі, вкажіть метадані теми і натисніть кнопку «Гаразд». Програма створить файл архіву ZIP із піктограмами теми. Ви можете розмістити його на форумі calibre на Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=166], а вже згодом розробник програми додасть її до списку вбудованих тем піктограм calibre. Типово, створену вами тему піктограм буде встановлено як поточну тему у calibre. Якщо ви тестуєте вашу тему, не забудьте вилучити зображення з теки resources/images, щоб програма використовувала саме піктограми з теми.

Починаючи з версії calibre 6, ви можете користуватися нетиповими піктограмами для світлого і темного режимів. Просто створіть дві версії піктограми і додайте до назви суфікс -for-dark-theme (темна тема) або -for-light-theme (світла тема). Наприклад, modified-for-dark-theme.png і modified-for-light-theme.png. Далі, calibre автоматично використовуватиме відповідну піктограму у поточній темі.

Налаштовування calibre за допомогою додатків

У calibre дуже багато модулів. Майже усі функціональні можливості calibre реалізовано у формі додатків. Додатки використовуються для перетворення даних, для отримання новин (у формі так званих рецептів), для різноманітних компонентів інтерфейсу користувача, для встановлення з’єднання з різними пристроями, для обробки файлів під час додавання їх до бібліотеки calibre тощо. Із повним списком вбудованих додатків calibre можна ознайомитися на сторінці Налаштування  →  Додатково  →  Додатки.

Ви можете створити власні додатки для налаштовування та розширення можливостей calibre. Архітектура додатків у calibre є дуже простою. Див. підручник Створення власних додатків для розширення функціональних можливостей calibre.

Готовий додаток можна вивантажити у гілку додатків calibre форуму Mobileread [https://www.mobileread.com/forums/forumdisplay.php?f=237]. Доступ до вивантажених додатків можна буде здійснювати за допомогою вбудованого засобу оновлення calibre.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Налаштування calibre »

- Документація з програмного інтерфейсу для роботи з додатками

Документація з програмного інтерфейсу для роботи з додатками

Визначає різноманітні абстрактні базові класи, з яких можна утворювати підкласи для створення потужних додатків. Корисні класи:

- Додаток

- FileTypePlugin

- Додати роботи з метаданими

- Додатки роботи з каталогом

- Додатки отримання метаданих

- Додатки перетворення даних

- Драйвери пристроїв

- Дії інтерфейсу користувача

- Додатки налаштувань

Додаток
class calibre.customize.Plugin(plugin_path)[source]

Додаток до calibre. Корисні учасники:

- self.installation_type: зберігає спосіб встановлення додатка.

- self.plugin_path: Зберігає шлях до файла ZIP, шо містить

цей додаток або None, якщо це вбудований додаток

- self.site_customization: зберігає введений рядок налаштовування

користувачем.

Методи, які слід перевизначити у підкласах:

- initialize()

- customization_help()

Корисні методи:

- temporary_file()

- __enter__()

- load_resources()

supported_platforms = []

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

name = 'Trivial Plugin'

Назва цього додатка. Щоб додаток працював, вам слід вказати якусь назву, не просто Trivial Plugin.

version = (1, 0, 0)

Версія цього додатка у форматі кортежу з трьох чисел (основний номер, додатковий номер, модифікація)

description = 'Не робить абсолютно нічого'

Короткий рядок із описом призначення додатка

author = 'Невідомий'

Автор цього додатка

priority = 1

Якщо для обробки певного типу файлів передбачено декілька додатків, додатки будуть запускатися за зменшенням пріоритетності, тобто першими буде запущено додатки з вищою пріоритетністю. Найвищою можливою пріоритетністю є sys.maxsize. Типовою пріоритетністю є 1.

minimum_calibre_version = (0, 4, 118)

Цей додаток потребує більш ранньої версії Calibre

installation_type = None

Спосіб встановлення додатка

can_be_disabled = True

Якщо матиме значення False, користувач не зможе вимкнути цей додаток. Користуйтеся із осторогою.

type = 'Основне'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

initialize()[source]

Викликається один раз під час ініціалізації додатків calibre. Додатки повторно ініціалізуватимуться кожного разу, коли додаватиметься новий додаток. Також зауважте, що якщо додаток запускається у робочому процесі, наприклад для додавання книг, додаток ініціалізуватиметься для кожного нового процесу обробки.

Тут виконується будь-яка специфічна для додатків ініціалізація, зокрема видобування ресурсів з файла ZIP додатка. Шлях до файла ZIP доступний як self.plugin_path.

Зауважте, що на цьому етапі self.site_customization є недоступним.

config_widget()[source]

Реалізуйте цей метод і save_settings() у вашому додатку, щоб скористатися нетиповим діалоговим вікном налаштування, а не покладатися на типове налаштовування на основі рядків.

Цей метод, якщо його реалізовано, має повертати QWidget. Віджет може мати необов’язковий метод validate(), який не приймає аргументів і викликається одразу після натискання користувачем кнопки «Гаразд». Зміни застосовуються, якщо і лише якщо метод повертає True.

Якщо з певної причини ви не змогли виконати налаштовування до цього часу, повернути кортеж з двох рядків (повідомлення, подробиці), ці дані буде показано як діалогове вікно попередження користувачеві, процес буде перервано.

save_settings(config_widget)[source]

Зберегти визначені користувачем за допомогою config_widget параметри.

Параметри:

config_widget – Віджет, який повертається config_widget().

do_user_config(parent=None)[source]

Цей метод показує діалогове вікно налаштувань цього додатка. Повертає True, якщо користувач натискає кнопку «Гаразд», і False, якщо ні. Зміни буде застосовано автоматично.

load_resources(names)[source]

Якщо цей додаток постачається у форматі файла ZIP (додаток, що встановлюється користувачем), цей метод дозволить вам завантажити ресурси з файла ZIP.

Наприклад, для завантаження зображення:

pixmap = QPixmap()
pixmap.loadFromData(self.load_resources(['images/icon.png'])['images/icon.png'])
icon = QIcon(pixmap)

Параметри:

names – Список шляхів до ресурсів у файлі ZIP з використанням роздільника /

Повертає:

Словник у формі {назва: вміст_файла}. Усі назви, які не було знайдено у файлі ZIP, не буде включено до словника.

customization_help(gui=False)[source]

Повертає рядок із довідкою щодо налаштовування цього додатка. Типово, надсилає сигнал NotImplementedError, який позначає, що додаток не потребує налаштовування.

Якщо ви повторно реалізуєте цей метод у вашому підкласі, додаток проситиме користувача ввести рядок для налаштовування цього додатка. Доступ до рядка налаштовування можна буде отримати у формі self.site_customization.

Налаштовуванням місця може бути будь-яким, наприклад, шлях до виконуваного файла на комп’ютері користувача.

Параметри:

gui – Якщо True, повертати довідку у форматі HTML, якщо ні, повертати довідку у простому текстовому форматі.

temporary_file(suffix)[source]

Повертає файлоподібний об’єкт, який є тимчасовим файлом у файловій системі. Цей файл залишатиметься доступним навіть після його закриття. Файл буде вилучено під час завершення роботи інтерпретатора коду. Скористайтеся учасником name повернутого об’єкта, щоб отримати доступу до повного шляху до створеного тимчасового файла.

Параметри:

suffix – Суфікс назви тимчасових файлів.

cli_main(args)[source]

Цей метод є основною точкою входу до інтерфейсу командного рядка додатка. Викликається, коли користувач віддає таку команду: calibre-debug -r «Назва додатка». Усі передані аргументи будуть зберігатися у змінній args.

FileTypePlugin
class calibre.customize.FileTypePlugin(plugin_path)[source]

Основа: Plugin

Додаток, який пов’язано із певним набором типів файлів.

file_types = {}

Набір типів файлів, для яких слід запускати додаток. Скористайтеся «*», якщо додаток слід запускати для усіх файлів. Приклад: {'lit', 'mobi', 'prc'}

on_import = False

Якщо True, цей додаток запускається, якщо до бази даних додаються книги.

on_postimport = False

Якщо True, цей додаток запускається після додавання книг до бази даних. У цьому випадку викликаються методи postimport і postadd додатка.

on_postconvert = False

Якщо True, цей додаток буде запущено після перетворення книги. У цьому випадку буде викликано метод postconvert додатка.

on_postdelete = False

Якщо True, цей додаток буде запущено після вилучення файла книги з бази даних. У цьому випадку буде викликано метод postdelete додатка.

on_preprocess = False

Якщо True, цей додаток запускається безпосередньо перед перетворенням.

on_postprocess = False

Якщо True, цей додаток запускається після перетворення для обробки остаточного файла, який буде створено додатку виведення даних перетворення.

type = 'Тип файлу'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

run(path_to_ebook)[source]

Запустити додаток. Має бути реалізовано у підкласах. Має вносити усі потрібні зміни до електронної книги і повертати абсолютний шлях до файла зміненої книги. Якщо вносити зміни не потрібно. Якщо станеться помилка, має повідомляти про виключення (Exception). Типова реалізація просто повертає шлях до початкового файла електронної книги. Зауважте, що шлях до початкового файла (до запуску будь-яких додатків), доступний як self.original_path_to_file.

Змінений файл електронної книги має бути створено за допомогою методу temporary_file().

Параметри:

path_to_ebook – Повний шлях до електронної книги.

Повертає:

Повний шлях до зміненої електронної книги.

postimport(book_id, book_format, db)[source]

Викликається після імпортування, тобто після того, як файл книги було додано до бази даних. Зауважте, що цей метод відрізняється від postadd(), який викликається, коли запис книги створюється вперше. Цей метод викликається кожного разу, коли до запису книги додається новий файл. Корисний для внесення змін до запису книги на основі вмісту нового доданого файла.

Параметри:

- book_id – ідентифікатор доданої книжки в базі даних.

- book_format – Тип файла доданої книги.

- db – База даних бібліотеки.

postconvert(book_id, book_format, db)[source]

Буде викликано після перетворення, тобто після того, як перетворений файл результатів буде додано до бази даних. Зауважте, що запуск відбуватиметься лише після перетворення, а не після додавання книги. Корисно для внесення змін до запису книги на основі вмісту щойно доданого файла.

Параметри:

- book_id – ідентифікатор доданої книжки в базі даних.

- book_format – Тип файла доданої книги.

- db – База даних бібліотеки.

postdelete(book_id, book_format, db)[source]

Буде викликано після вилучення, тобто після того, як файл книги буде вилучено з бази даних. Зауважте, що запуск відбуватиметься не після вилучення запису книги, а лише після того, як буде вилучено один або декілька форматів книги. Корисно для внесення змін до запису книги на основі формату вилученого файла.

Параметри:

- book_id – ідентифікатор доданої книжки в базі даних.

- book_format – Тип файла доданої книги.

- db – База даних бібліотеки.

postadd(book_id, fmt_map, db)[source]

Викликається після додавання (add), тобто після того, як книгу буде додано до бази даних. Зауважте, що цей метод відрізняється від postimport(), який викликається після додавання окремого файла до запису книги. postadd() викликається, лише якщо вперше створюється увесь запис книги, можливо з декількома файлами книги. Корисно, якщо вам потрібно внести зміни до запису книги у базі даних, якщо книгу вперше додано до calibre.

Параметри:

- book_id – ідентифікатор доданої книжки в базі даних.

- fmt_map – Прив’язка формату файла до шляху, з якого було додано цей формат файла. Зауважте, що цей шлях може і не вказувати на наявний файл, оскільки іноді файли додаються як потоки даних. У такому випадку значення шляху може бути фіктивним або таким, якого не існує.

- db – База даних бібліотеки

Додати роботи з метаданими

class calibre.customize.MetadataReaderPlugin(*args, **kwargs)[source]

Основа: Plugin

Додаток, який реалізує читання метаданих із файлів певного набору типів.

file_types = {}

Набір типів файлів, для яких слід запускати додаток. Приклад: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

version = (7, 10, 0)

Версія цього додатка у форматі кортежу з трьох чисел (основний номер, додатковий номер, модифікація)

author = 'Kovid Goyal'

Автор цього додатка

type = 'Читання метаданих'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

get_metadata(stream, type)[source]

Повертає метадані для файла, представленого потоком даних (файлоподібним об’єктом, для якого передбачено підтримку читання). Надсилає повідомлення щодо виключення, якщо сталася помилка із вхідними даними.

Параметри:

type – тип файла. Гарантовано є одним із записів у file_types.

Повертає:

A calibre.ebooks.metadata.book.Metadata object

class calibre.customize.MetadataWriterPlugin(*args, **kwargs)[source]

Основа: Plugin

Додаток, який реалізує читання метаданих із файлів певного набору типів.

file_types = {}

Набір типів файлів, для яких слід запускати додаток. Приклад: set(['lit', 'mobi', 'prc'])

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

version = (7, 10, 0)

Версія цього додатка у форматі кортежу з трьох чисел (основний номер, додатковий номер, модифікація)

author = 'Kovid Goyal'

Автор цього додатка

type = 'Запис метаданих'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

set_metadata(stream, mi, type)[source]

Встановлює метадані для файла, представленого потоком даних (файлоподібним об’єктом, для якого передбачено підтримку читання). Надсилає повідомлення щодо виключення, якщо сталася помилка із вхідними даними.

Параметри:

- type – тип файла. Гарантовано є одним із записів у file_types.

- mi – A calibre.ebooks.metadata.book.Metadata object

Додатки роботи з каталогом

class calibre.customize.CatalogPlugin(plugin_path)[source]

Основа: Plugin

Додаток, який реалізує засіб створення каталогу.

file_types = {}

Вивести тип файлів, для яких слід запускати додаток. Приклад: „epub“ або „xml“

type = 'Генератор каталогу'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

cli_options = []

Параметри обробки інтерфейсу командного рядка, специфічні для цього додатка, оголошені як Option у namedtuple:

from collections import namedtuple
Option = namedtuple(„Option“, „option, default, dest, help“)
cli_options = [Option(“–catalog-title“, default = „My Catalog“, dest = „catalog_title“, help = (_(„Title of generated catalog. nDefault:“) + « „» + „%default“ + «“»))]
cli_options parsed in calibre.db.cli.cmd_catalog:option_parser()

initialize()[source]

Якщо додаток не є вбудованим, копіює файли .ui і .py з файла ZIP до $TMPDIR. Вкладку буде створено динамічно і додано до діалогового вікна параметрів каталогу у calibre.gui2.dialogs.catalog.py:Catalog

run(path_to_output, opts, db, ids, notification=None)[source]

Запускає додаток. Має бути реалізовано у підкласах. Має створити каталог у форматі, вказаному у file_types, повернувши абсолютний шлях до створеного файла каталогу. Якщо станеться помилка, має надіслати сигнал Exception.

Створений файл каталогу має бути створено за допомогою методу temporary_file().

Параметри:

- path_to_output – Абсолютний шлях до створеного файла каталогу.

- opts – Словник аргументів ключових слів

- db – Об’єкт LibraryDatabase2

Додатки отримання метаданих
class calibre.ebooks.metadata.sources.base.Source(*args, **kwargs)[source]

Основа: Plugin

type = 'Джерело метаданих'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

author = 'Kovid Goyal'

Автор цього додатка

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

capabilities = frozenset({})

Набір можливостей, підтримуваних цим додатком. Корисні можливості: «identify», «cover»

touched_fields = frozenset({})

Список полів метаданих, які потенційно може бути отримано цим додатком під час фази ідентифікації.

has_html_comments = False

Встановіть значення True, якщо ваш додаток повертає форматовані у HTML коментарі.

supports_gzip_transfer_encoding = False

Встановлення для цього параметра значення True означає, що об’єкт навігатора (browser) вказуватиме, що у ньому передбачено підтримку передавання даних зі стисканням gzip. Це може пришвидшити отримання даних, але варто спочатку пересвідчитися, що джерело справді може надавати дані у форматі gzip належним чином.

ignore_ssl_errors = False

Встановіть значення True, якщо помилки, пов’язані із сертифікатами HTTPS, слід ігнорувати для цього джерела.

cached_cover_url_is_reliable = True

Кешовані адреси зображень обкладинок іноді можуть стати неактуальними (тобто обкладинка стає недоступно до отримання або повернуте зображення є непридатним до використання). Якщо таке трапляється часто з цим джерелом, встановіть значення False.

options = ()

Список об’єктів Option. Об’єкти буде використано для автоматичної побудови віджета налаштовування для цього додатка.

config_help_message = None

Рядок, який буде показано у верхній частині віджета налаштовування цього додатка.

can_get_multiple_covers = False

Якщо True, це джерело може повертати декілька зображень обкладинок у відповідь на заданий запит.

auto_trim_covers = False

Якщо встановлено значення True, зображення обкладинок, отримані цим додатком, буде автоматично обрізано.

prefer_results_with_isbn = True

Якщо встановлено значення True і це джерело повертає декілька результатів у відповідь на один запит, деякі з яких містять дані щодо ISBN, а деякі — ні, результати без ISBN буде проігноровано.

is_configured()[source]

Повертає False, якщо ваш додаток має бути налаштовано перед використанням. Наприклад, йому потрібне ім’я користувача, пароль або ключ до програмного інтерфейсу.

customization_help()[source]

Повертає рядок із довідкою щодо налаштовування цього додатка. Типово, надсилає сигнал NotImplementedError, який позначає, що додаток не потребує налаштовування.

Якщо ви повторно реалізуєте цей метод у вашому підкласі, додаток проситиме користувача ввести рядок для налаштовування цього додатка. Доступ до рядка налаштовування можна буде отримати у формі self.site_customization.

Налаштовуванням місця може бути будь-яким, наприклад, шлях до виконуваного файла на комп’ютері користувача.

Параметри:

gui – Якщо True, повертати довідку у форматі HTML, якщо ні, повертати довідку у простому текстовому форматі.

config_widget()[source]

Реалізуйте цей метод і save_settings() у вашому додатку, щоб скористатися нетиповим діалоговим вікном налаштування, а не покладатися на типове налаштовування на основі рядків.

Цей метод, якщо його реалізовано, має повертати QWidget. Віджет може мати необов’язковий метод validate(), який не приймає аргументів і викликається одразу після натискання користувачем кнопки «Гаразд». Зміни застосовуються, якщо і лише якщо метод повертає True.

Якщо з певної причини ви не змогли виконати налаштовування до цього часу, повернути кортеж з двох рядків (повідомлення, подробиці), ці дані буде показано як діалогове вікно попередження користувачеві, процес буде перервано.

save_settings(config_widget)[source]

Зберегти визначені користувачем за допомогою config_widget параметри.

Параметри:

config_widget – Віджет, який повертається config_widget().

get_author_tokens(authors, only_first_author=True)[source]

Отримує список авторів і повертає список ключів, корисних для створення запитів щодо пошуку із логікою «І». Ця функція намагається повернути ключі у порядку «ім’я по-батькові прізвище», припускаючи, що якщо у імені автора є кома, ім’я записано у формі «прізвище, решта імені».

get_title_tokens(title, strip_joiners=True, strip_subtitle=False)[source]

Отримує заголовок і повертає список ключів, які потрібні для запиту щодо пошуку з логікою І. Включає з’єднання (необов’язково) і пунктуацію.

split_jobs(jobs, num)[source]

Поділити список завдань на не більше за num груп, наскільки можна однаково.

test_fields(mi)[source]

Повертає перше поле з self.touched_fields, яке є нулем у об’єкті mi.

clean_downloaded_metadata(mi)[source]

Викличте цей метод у вашому методі identify додатка для нормалізації метаданих до передавання об’єкта Metadata до result_queue. Ви, звичайно ж, можете скористатися нетиповим алгоритмом, який відповідатиме вашому джерелу метаданих.

get_book_url(identifiers)[source]

Повертає триелементний кортеж або None. Триелементний кортеж записується у такому форматі: (тип ідентифікатора, значення ідентифікатора, адреса). Адресою є адреса (URL) книги, що визначається за ідентифікаторами у цьому джерелі. Тип ідентифікатора та значення ідентифікатора визначають ідентифікатор, що відповідає адресі. Адреса має бути придатною для переходу за нею користувача за допомогою програми для перегляду сторінок інтернету. Її призначено для створення придатного до натискання посилання для відвідання користувачем сторінки книги у цьому джерелі. Якщо адреси не буде знайдено, буде повернуто None. Цей метод має працювати швидко і послідовно, отже, вам слід реалізовувати його, лише якщо можна побудувати адресу на основі відомих ідентифікаторів з бази даних.

get_book_url_name(idtype, idval, url)[source]

Повернути назву у зручному для читанні форматі із повернутого значення get_book_url().

get_book_urls(identifiers)[source]

Перевизначте цей метод, якщо для цієї книги слід повернути декілька адрес. Повертає список з кортежів, що складаються з 3 значень. Типово, цей метод просто викликає get_book_url().

get_cached_cover_url(identifiers)[source]

Повертає кешовану адресу зображення обкладинки для книги, вказаної словником ідентифікаторів, або None, якщо такої адреси не існує.

Зауважте, що цей метод має повертати лише перевірені адреси, тобто не ті адреси, використання яких може призвести до використання типового зображення обкладинки або які не може бути знайдено.

id_from_url(url)[source]

Обробити адресу і повернути кортеж у форматі (тип_ідентифікатора, значення_ідентифікатора). Якщо адреса не відповідає взірцю для джерела метаданих, повернути None.

identify_results_keygen(title=None, authors=None, identifiers={})[source]

Повертає функцію, яка використовується для створення ключа, який може упорядковувати об’єкти Metadata за відповідністю вказаному критерію пошуку (назві, авторам, ідентифікаторам).

Ці ключі використовуються для упорядковування результатів виклику identify().

Докладніше про типовий алгоритм можна дізнатися з документації до InternalMetadataCompareKeyGen. Повторно реалізуйте цю функцію у вашому додатку, якщо типовий алгоритм є непридатним.

identify(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30)[source]

Ідентифікувати книжку за назвою/автором/ISBN

Якщо вказано ідентифікатори і не знайдено відповідників, а це джерело метаданих не зберігає усіх пов’язаних ідентифікаторів (наприклад, усіх ISBN книги), цей метод має повторити спробу на основі лише даних про назву та автора (припускаючи, що ці дані вказано).

Якщо це джерело метаданих також надає зображення обкладинок, адресу обкладинки має бути кешовано так, щоб наступний виклик функції отримання даних з тими самими даними щодо ISBN або спеціального ідентифікатор не потребував повторного отримання адреси обкладинки. Для цього використовуйте програмний інтерфейс кешування.

Усі об’єкти Metadata, які додаються до result_queue за допомогою цього методу, повинні мати атрибут source_relevance, тобто ціле число, яке позначає порядок, у якому слід повертати результати з цього джерела метаданих для цього запиту. Це ціле число буде використано методом compare_identify_results(). Якщо порядок не є важливим, встановіть нульове значення для усіх результатів.

Переконайтеся, що усі дані щодо прив’язки до обкладинки або ISBN буде кешовано до того, як об’єкт Metadata буде передано до result_queue.

Параметри:

- log – Об’єкт журналу. Використовуйте його для виведення діагностичної інформації і даних щодо помилок.

- result_queue – Черга результатів, до неї додаються результати. Кожен результат є об’єктом Metadata.

- abort – Якщо abort.is_set() повертає True, перервати подальшу обробку і повернути керування якомога швидше.

- title – Назва (заголовок) книги, може бути None

- authors – Список авторів книги, може бути None

- identifiers – Словник інших ідентифікаторів, найчастіше {„isbn“:“1234…“}

- timeout – Час очікування у секундах. Жоден запит до мережі має бути виконано протягом часу, який визначається часом очікування.

Повертає:

None, якщо помилок не було. Якщо сталися помилки, буде виведено представлення unicode повідомлення про помилку для показу користувачеві.

download_cover(log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=30, get_best_cover=False)[source]

Отримати обкладинку і додати її до result_queue. Параметри усі мають те саме значення, що і для identify(). Додати (self, cover_data) до result_queue.

Якщо можна, цей метод має використовувати кешовані адреси обкладинок. Якщо кешованих даних немає, більшість додатків просто викликають identify і використовують її результати.

Якщо параметр get_best_cover має значення True і цей додаток може отримувати декілька обкладинок, він має отримувати лише «найкращу».

class calibre.ebooks.metadata.sources.base.InternalMetadataCompareKeyGen(mi, source_plugin, title, authors, identifiers)[source]

Створює ключ упорядковування для порівняння відповідності об’єктів Metadata за заданим запитом щодо пошуку. Використовується лише для порівняння результатів з одного джерела метаданих, а не з різних джерел.

Ключ упорядковування забезпечує відповідність упорядковування за зростанням упорядковуванню за спаданням відповідності.

Алгоритм:

- Надавати перевагу результатам, які мають принаймні один ідентифікатор, який збігається із запитом.

- Пріоритет результатів із кешованою адресою зображення обкладинки

- Пріоритет результатів, у яких усі поля заповнено

- Надавати перевагу результатам тією самою мовою, що і поточна мова інтерфейсу користувача.

- Пріоритет результатів, які повністю збігаються за назвою із запитом

- Надавати перевагу результатам із довшими коментарями (понад 10% довшими)

- Використовувати відповідність результатів, яку повідомляє засіб пошуку джерела метаданих

engine

Додатки перетворення даних
class calibre.customize.conversion.InputFormatPlugin(*args)[source]

Основа: Plugin

Додатки InputFormatPlugins відповідають за перетворення документів у набір даних HTML+OPF+CSS+інше. Результати перетворення мають зберігатися у кодуванні UTF-8. Основна дія відбувається у convert().

type = 'Вхід перетворення'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

can_be_disabled = False

Якщо матиме значення False, користувач не зможе вимкнути цей додаток. Користуйтеся із осторогою.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

file_types = {}

Набір типів файлів, для яких слід запускати додаток. Приклад: set(['azw', 'mobi', 'prc'])

is_image_collection = False

Якщо має значення True, цей додаток вхідних даних створює збірку зображень, по одному на файл HTML. Можна встановлювати під час роботи додатка, у методі convert, якщо файли вхідних даних можуть бути як збірками зображень, так і збірками без зображень. Якщо ви встановите значення True, вам слід реалізувати метод get_images(), який повертатиме список зображень.

core_usage = 1

Кількість ядер процесора, які використовуються цим додатком. Значення «-1» означає, що слід використовувати усі доступні ядра.

for_viewer = False

Якщо встановлено значення True, додаток обробки вхідних даних виконуватиме спеціальну обробку, щоб зробити виведені дані придатними для перегляду.

output_encoding = 'utf-8'

Кодування, у якому цей додаток створює файли. Значення None означає, що кодування не визначено, і його слід визначити окремо.

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Параметри є спільними для усіх додатків вхідних форматів даних. Не перевизначайте їх у підкласах. Замість цього, скористайтеся options. Кожен параметр має бути екземпляром OptionRecommendation.

options = {}

Параметри для налаштовування поведінки цього додатка. Кожен параметр має бути екземпляром OptionRecommendation.

recommendations = {}

Набір кортежів з 3 елементів у формі (назва_параметра, рекомендоване_значення, рівень_рекомендування)

get_images()[source]

Повернути список абсолютних шляхів до зображень, якщо цей вхідні дані є збіркою зображень. Список зображень формується у тому самому порядку, що і у вмісті та таблиці змісту.

convert(stream, options, file_ext, log, accelerators)[source]

Цей метод має бути реалізовано у підкласах. Має повертати шлях до створеного файла OPF або екземпляр OEBBook. Усі виведені дані мають міститися у поточній теці. Якщо цей додаток створює файли поза поточною текою, їх має бути вилучено або позначено для вилучення до повернення керування цим методом.

Параметри:

- stream – Файл як об’єкт, який містить дані файла вхідних даних.

- options – Параметри для налаштовування процесу перетворення. Гарантовано має атрибути, що відповідають усім параметрам, оголошеним цим додатком. Крім того, має атрибут verbose, який приймає інтегральні дані від нуля. Більші значення атрибута означають докладніше виведення діагностичних даних. Іншим корисним атрибутом є input_profile, який є екземпляром calibre.customize.profiles.InputProfile.

- file_ext – Суфікс назви (без крапки) файла вхідних даних. Гарантовано має належати набору значень file_types, підтримку яких передбачено у додатку.

- log – Об’єкт calibre.utils.logging.Log. Усе виведення має використовувати цей об’єкт.

- accelarators – Словник різноманітних даних, доступ до якого має бути простим для додатка вхідних даних. Пришвидшує наступні етапи перетворення.

postprocess_book(oeb, opts, log)[source]

Викликається для уможливлення виконання додатком вхідних даних остаточної обробки після основного етапу обробки даних книги.

specialize(oeb, opts, log, output_fmt)[source]

Викликається для того, щоб надати змогу додатку спеціалізувати оброблену книгу для певного формату виведення даних. Викликається після postprocess_book і перед будь-якими перетвореннями, які виконуються над обробленою книгою.

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Викликається для створення віджета, який використовується для налаштовування цього додатка у графічному інтерфейсі calibre. Віджет має бути екземпляром класу PluginWidget. Приклади можна знайти у вбудованих додатках обробки вхідних даних.

class calibre.customize.conversion.OutputFormatPlugin(*args)[source]

Основа: Plugin

OutputFormatPlugins відповідають за перетворення документа OEB (OPF+HTML) на остаточну електронну книгу.

Можна припускати, що документ OEB має кодування UTF-8. Основна дія відбувається у convert().

type = 'Вивід перетворення'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

can_be_disabled = False

Якщо матиме значення False, користувач не зможе вимкнути цей додаток. Користуйтеся із осторогою.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

file_type = None

Тип файлів (суфікс назви без крапки), до якого виводить дані цей додаток

common_options = {<calibre.customize.conversion.OptionRecommendation object>}

Параметри є спільними для усіх додатків вхідних форматів даних. Не перевизначайте їх у підкласах. Замість цього, скористайтеся options. Кожен параметр має бути екземпляром OptionRecommendation.

options = {}

Параметри для налаштовування поведінки цього додатка. Кожен параметр має бути екземпляром OptionRecommendation.

recommendations = {}

Набір кортежів з 3 елементів у формі (назва_параметра, рекомендоване_значення, рівень_рекомендування)

property description

str(object=““) -> str str(байти_або_буфер[, кодування[, помилки]]) -> str

Створює рядковий об’єкт з вказаного об’єкта. Якщо вказано «кодування » або «помилки», об’єкт має розкривати буфер даних, який буде декодовано з використанням вказаного кодування та обробника помилок. Якщо ці параметри не вказано, повертає результат object.__str__() (if defined) або repr(object). Типовим значенням «кодування» є sys.getdefaultencoding(). Типовим значенням «помилки» є «strict».

convert(oeb_book, output, input_plugin, opts, log)[source]

Обробити вміст oeb_book (екземпляра calibre.ebooks.oeb.OEBBook) до файла, вказаного параметром виведення.

Параметри:

- output – Файлоподібний об’єкт або рядок. Якщо це рядок, він вважається шляхом до каталогу, який може і не існувати. Додаток виведення даних має записати виведені дані до цієї теки. Якщо це файлоподібний об’єкт, додаток виведення має записати свої дані до цього файла.

- input_plugin – Додаток обробки вхідних даних, який було використано на початку конвеєра перетворення.

- opts – Параметри перетворення. Гарантовано мають атрибути, які відповідають OptionRecommendations цього додатка.

- log – Засіб журналювання. За допомогою цього можна виводити діагностичні та інші повідомлення.

specialize_options(log, opts, input_fmt)[source]

Можна скористатися для зміни значень параметрів перетворення, які використовуються у конвеєрі перетворення.

specialize_css_for_output(log, opts, item, stylizer)[source]

Можна використовувати для внесення змін до CSS під час процедури спрощення CSS.

Параметри:

- item – Запис (файл HTML), який обробляється.

- stylizer – Об’єкт Stylizer, який містить спрощені стилі для запису. Отримати стиль для будь-якого елемента можна за допомогою stylizer.style(елемент).

gui_configuration_widget(parent, get_option_by_name, get_option_help, db, book_id=None)[source]

Викликається для створення віджета, який використовується для налаштовування цього додатка у графічному інтерфейсі calibre. Віджет має бути екземпляром класу PluginWidget. Приклади можна знайти у вбудованих додатках для виведення даних.

Драйвери пристроїв

Базовим класом для всіх драйверів пристроїв є DevicePlugin. Втім, якщо пристрій представляє себе операційній системі як диск USBMS, вам слід використовувати замість нього клас USBMS, оскільки у ньому реалізовано усю логіку, потрібну для цього типу пристроїв.

class calibre.devices.interface.DevicePlugin(plugin_path)[source]

Основа: Plugin

Визначає інтерфейс, який має бути реалізовано модулями обробки, які обмінюються даними з пристроєм для читання електронних книг.

type = 'Інтерфейс пристрою'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

FORMATS = ['lrf', 'rtf', 'pdf', 'txt']

Упорядкований список підтримуваних форматів

VENDOR_ID = 0

VENDOR_ID може бути або цілим числом, або списком цілих чисел, або словником. Якщо це словник, він має бути словником словників у такій формі:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Ціле число або список цілих чисел

BCD = None

BCD може бути або None для розрізнення між пристроями, які засновано на BCD, або списком номерів BCD усіх пристроїв, підтримку яких передбачено у цьому драйвері.

THUMBNAIL_HEIGHT = 68

Висота мініатюр на пристрої

THUMBNAIL_COMPRESSION_QUALITY = 75

Якість стискання мініатюр. Встановіть близьке до 100 значення, щоб мати мініатюри вищої якості із меншою кількістю викривлень стискання. Звичайно ж, при цьому мініатюри матимуть більший розмір.

WANTS_UPDATED_THUMBNAILS = False

Встановіть для цього параметра значення True, якщо на пристрої передбачено оновлення мініатюр зображень обкладинок під час sync_booklists. Встановлення значення true накаже device.py оновлювати мініатюри обкладинок під час встановлення відповідності книг.

CAN_SET_METADATA = ['title', 'authors', 'collections']

Визначає, чи може бути встановлено метадані для книг за допомогою графічного інтерфейсу.

CAN_DO_DEVICE_DB_PLUGBOARD = False

Визначає, чи може пристрій обробляти набори метаданих device_db.

path_sep = '/'

Роздільник у шляху для шляхів до книг на пристрої

icon = 'reader.png'

Піктограма для цього пристрою

UserAnnotation

alias of Annotation

OPEN_FEEDBACK_MESSAGE = None

Якщо має значення, відмінне від None на смужці стану, графічний інтерфейс показує цей рядок як повідомлення. Корисно, якщо відкриття даних є довготривалим.

VIRTUAL_BOOK_EXTENSIONS = frozenset({})

Набір суфіксів файлів, які є на пристрої «віртуальними книгами», і тому їх не можна переглядати, зберігати або додавати до бібліотеки. Приклад: frozenset(['kobo'])

VIRTUAL_BOOK_EXTENSION_MESSAGE = None

Повідомлення, яке слід показувати користувачеві для файлів із суфіксами назв віртуальних книг.

NUKE_COMMENTS = None

Визначає, чи слід вилучати коментарі у копії книги, яка надсилається на пристрій. Якщо має значення, відмінне від None, це має бути коротким рядком, яким буде замінено коментарі.

MANAGES_DEVICE_PRESENCE = False

Якщо має значення True, позначає, що цей драйвер повністю керує виявленням пристроїв, вилученням пристроїв тощо. Якщо ви встановите значення True, вам слід реалізувати методи detect_managed_devices і debug_managed_device_detection. Драйвер зі встановленим значенням True є відповідальним за виявлення пристроїв, керування «чорним» списком пристроїв, списком вилучених пристроїв тощо. calibre періодично викликатиме метод detect_managed_devices() і, якщо він повертатиме виявлений пристрій, calibre викликатиме open(). open() буде викликано кожного разу після повернення пристрою, навіть якщо попередні виклики open() завершувалися помилками, тому драйвер має підтримувати актуальність власного «чорного» списку пристроїв із помилками. Так само, під час вилучення пристрою calibre викличе eject(), а потім, припускаючи, що наступний виклик detect_managed_devices() повертає None, програма викличе post_yank_cleanup().

SLOW_DRIVEINFO = False

Якщо встановлено значення True, calibre викликатиме метод get_driveinfo() після завантаження списків книг для отримання даних щодо диска (driveinfo).

ASK_TO_ALLOW_CONNECT = False

Якщо встановлено значення True, calibre питатиме користувача про те, чи хоче він керувати пристроєм за допомогою calibre під час першого виявлення пристрою. Якщо ви встановите значення True, вам доведеться реалізувати get_device_uid(), ignore_connected_device(), get_user_blacklisted_devices() та set_user_blacklisted_devices()

user_feedback_after_callback = None

Встановіть значення у форматі словника {„title“:title, „msg“:msg, „det_msg“:detailed_msg}, щоб програма calibre показувала повідомлення користувачеві після певних зворотних викликів (у поточній версії лише upload_books). Спробуйте не набридати користувачеві повідомленнями. Значення цієї змінної перевірятиметься після кожного зворотного виклику, тому вам слід встановлювати його, лише якщо це справді потрібно.

classmethod get_open_popup_message()[source]

У графічному інтерфейсі буде показано як немодальне контекстне вікно. Має бути екземпляром OpenPopupMessage

is_usb_connected(devices_on_system, debug=False, only_presence=False)[source]

Повертає True, device_info, якщо пристрій, який обробляється цим додатком, з’єднано з комп’ютером.

Параметри:

devices_on_system – Поточний список з’єднаних пристроїв

detect_managed_devices(devices_on_system, force_refresh=False)[source]

Викликається, лише якщо MANAGES_DEVICE_PRESENCE має значення True.

Шукати пристрої, з якими може працювати цей драйвер. Має повернути об’єкт пристрою, якщо пристрій буде знайдено. Цей об’єкт буде передано до методу open() як connected_device. Якщо пристрій знайдено не буде, повертає None. Повернутий об’єкт може бути будь-чим — calibre його не використовує, просто передає open().

Цей метод періодично викликається графічним інтерфейсом програми, тому він має бути не надто вибагливим до ресурсів. Скористайтеся кешуванням, щоб уникнути регулярного сканування системи.

Параметри:

- devices_on_system – Набір пристроїв USB, які виявлено у системі.

- force_refresh – Якщо має значення True, і драйвер використовує кешування для запобігання повторним скануванням, кеш має бути спорожнено.

debug_managed_device_detection(devices_on_system, output)[source]

Викликається, лише якщо MANAGES_DEVICE_PRESENCE має значення True.

Має записувати дані щодо пристроїв, які виявлено у системі, до виведення, яке є файлоподібним об’єктом.

Має повернути True, якщо пристрій було виявлено і успішно відкрито. Якщо цього не відбулося, має повернути False.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Параметри:

- key – Ключ для розблокування пристрою

- log_packets – Якщо true, потік пакетів на пристрій і з пристрою записується до журналу.

- report_progress – Функція, яка викликається із відсотком виконання (числом від 0 до 100) для виконання різних завдань. Якщо викликано з -1, програма вважатиме, що завдання не має даних щодо поступу виконання.

- detected_device – Дані щодо пристрою від засобу сканування пристроїв

can_handle_windows(usbdevice, debug=False)[source]

Необов’язковий метод для виконання подальших перевірок для пристрою з метою переконатися, що цей драйвер може з ним працювати. Якщо це не так, має повертати False. Цей метод викликається лише після встановлення відповідності виробника, ідентифікаторів продукту та bcd, отже він може виконувати доволі тривалі за часом перевірки. Типова реалізація завжди повертає True. Цей метод викликається лише у Windows. Див. також can_handle().

Зауважте, що для пристроїв на основі USBMS це метод типово делегується can_handle(). Отже, вам слід лише перевизначити can_handle() у вашому підкласі USBMS.

Параметри:

usbdevice – Пристрій USB (usbdevice) у форматі, який повертає calibre.devices.winusb.scan_usb_devices()

can_handle(device_info, debug=False)[source]

Версія Unix can_handle_windows().

Параметри:

device_info – Є кортежем (vid, pid, bcd, виробник, продукт, серійний номер)

open(connected_device, library_uuid)[source]

Виконати усі специфічні для пристрою дії з ініціалізації. Викликається після виявлення пристрою, але перед усіма іншими функціями, які обмінюються даними з пристроєм. Приклад: для пристроїв, які представляються як сховища даних із інтерфейсом USB, цей метод має відповідати за монтування пристрою або, якщо пристрій змонтовано у автоматичному режимі, за пошук точки монтування. Метод calibre.devices.usbms.device.Device.open() містить реалізацію цієї функції і має слугувати добрим прикладом для реалізації доступу до сховищ даних із інтерфейсом USB.

Цей метод може викликати виключення OpenFeedback для показу повідомлення користувачеві.

Параметри:

- connected_device – Пристрій, який ми намагаємося відкрити. Є кортежем (ідентифікатор виробника, ідентифікатор продукту, назва виробника, серійний номер пристрою). Втім, у деяких пристроїв немає серійного номера, а у Windows доступні лише перші три поля, останнє подається як None.

- library_uuid – UUID поточної бібліотеки calibre. Може мати значення None, якщо бібліотеки немає (наприклад, якщо використано з командного рядка).

eject()[source]

Демонтувати і вилучити запис пристрою з операційної системи. Наявність завдань з графічним інтерфейсом, які продовжують виконувати завдання з пристроєм, при цьому не перевіряється.

ЗАУВАЖЕННЯ: цей метод не можна викликати у тому самому потоці виконання, що і решта методів роботи з пристроями.

post_yank_cleanup()[source]

Викликається, якщо користувач від’єднує пристрій без його вилучення з системи.

set_progress_reporter(report_progress)[source]

Встановлює функцію для надання даних щодо поступу виконання.

Параметри:

report_progress – Функція, яка викликається із відсотком виконання (числом від 0 до 100) для виконання різних завдань. Якщо викликано з -1, програма вважатиме, що завдання не має даних щодо поступу виконання.

get_device_information(end_session=True)[source]

Опитує пристрій щодо даних про нього. Див. L{DeviceInfoQuery}.

Повертає:

(назва пристрою, версія пристрою, версія програмного забезпечення на пристрої, тип MIME) Кортеж даних може містити п’ятий елемент, який є словником із даними щодо диска. Див. приклад у usbms.driver.

get_driveinfo()[source]

Повертає словник driveinfo. Зазвичай, викликається з get_device_information(), але якщо завантаження driveinfo є повільним для цього драйвера, він має встановити SLOW_DRIVEINFO. У цьому випадку, функцію буде викликано calibre після завантаження списків книг. Зауважте, що метод викликається не у потоці обробки пристрою, отже, драйвер має кешувати дані щодо диска у методі books(), а ця функція має повернути кешовані дані.

card_prefix(end_session=True)[source]

Повертає двоелементний список префіксів до шляхів на картках. Якщо картки немає, для префікса картки буде встановлено значення None. Приклади: („/місце“, „/місце2“) (None, „місце2“) („місце“, None) (None, None)

total_space(end_session=True)[source]

Отримати загальний об’єм доступного місця на точках монтування:

1. Основна пам’ять

2. Картка пам’яті A

3. Картка пам’яті B

Повертає:

Триелементних список із загальним об’ємом у байтах для (1, 2, 3). Якщо за якоюсь із адрес пристрою немає, має повернути 0.

free_space(end_session=True)[source]

Отримати об’єм вільного місця на точках монтування:

1. Основна пам’ять

2. Картка A

3. Картка B

Повертає:

Триелементних список із вільним об’ємом у байтах для (1, 2, 3). Якщо за якоюсь із адрес пристрою немає, має повернути -1.

books(oncard=None, end_session=True)[source]

Повертає список електронних книг на пристрої.

Параметри:

oncard – Якщо «carda» або «cardb», повертає список електронних книг на вказаній картці пам’яті. Якщо інше значення, повертає список електронних книг у основній пам’яті пристрою. Якщо вказано картку, але на картці немає книг, повертає порожній список.

Повертає:

BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Вивантажити список книг на пристрій. Якщо якийсь файл вже існує на пристрої, його буде замінено. Цей метод має повідомляти про FreeSpaceError, якщо на пристрої недостатньо вільного місця. Текст FreeSpaceError має містити слово «card», якщо on_card має значення, відмінне від None. Інакше, він має містити слово «memory».

Параметри:

- files – Список шляхів

- names – Список назв файлів, які повинні мати книги після вивантаження на пристрій. len(names) == len(files)

- metadata – Якщо має значення, яке є відмінним від None, є списком об’єктів Metadata. Ідея полягає у використанні метаданих для визначення, куди на пристрої слід зберегти книгу. len(metadata) == len(files). Окрім типового зображення обкладинки (шляху до зображення), може також існувати атрибут мініатюри, який матиме вищий пріоритет. Атрибут мініатюри записується у форматі (ширина, висота, дані обкладинки у форматі jpeg).

Повертає:

Список триелементни кортежів. Список призначено для передавання до add_books_to_metadata().

classmethod add_books_to_metadata(locations, metadata, booklists)[source]

Додає адреси до списків книг. Ця функція не повинна обмінюватися даними з пристроєм.

Параметри:

- locations – Результати виклику L{upload_books}

- metadata – Список об’єктів Metadata, той самий, що і для upload_books().

- booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

delete_books(paths, end_session=True)[source]

Вилучити книги за вказаними шляхами на пристрої.

classmethod remove_books_from_metadata(paths, booklists)[source]

Вилучити книги зі списку метаданих. Ця функція не повинна обмінюватися даними з пристроєм.

Параметри:

- paths – шляхи до книг на пристрої.

- booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

sync_booklists(booklists, end_session=True)[source]

Оновити метадані на пристрої.

Параметри:

booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

get_file(path, outfile, end_session=True)[source]

Прочитати файл з адресою path на пристрої і записати його вміст до outfile.

Параметри:

outfile – файлових об’єкт, наприклад sys.stdout, або результат виклику open().

classmethod config_widget()[source]

Має повернути QWidget. QWidget містить параметри для інтерфейсу пристрою.

classmethod save_settings(settings_widget)[source]

Має зберегти параметри на диск. Отримує віджет, створений у config_widget(), із зберігає усі параметри на диск.

classmethod settings()[source]

Має повернути об’єкт opts. Об’єкт opts повинен мати принаймні один атрибут format_map, який є упорядкованим списком форматів для пристрою.

set_plugboards(plugboards, pb_func)[source]

надає драйверу поточний набір метаданих та функцію для вибору певного набору. Цей метод викликається безпосередньо перед add_books і sync_booklists.

pb_func є придатною до виконання функцією із таким підписом:

def pb_func(назва_пристрою, формат, засоби обробки метаданих)

Ви передаєте функції назву поточного пристрою (як назву класу або як DEVICE_PLUGBOARD_NAME), формат, який вам потрібен («справжній» формат або «device_db»), і засоби обробки метаданих (ви вказали їх за допомогою set_plugboards там, де ви отримали цей метод).

Повертає:

None або окремий екземпляр засобу обробки метаданих.

set_driveinfo_name(location_code, name)[source]

Встановити назву пристрою у файлі driveinfo у значення «name». Цей параметр не змінюється, аж доки файл не буде повторно створено або його назву не буде знову змінено.

Недискові пристрої мають реалізовувати цей метод на основі кодів розташування, які повертаються методом get_device_information().

prepare_addable_books(paths)[source]

За вказаним списком шляхів повертає інший список шляхів. Ці шляхи вказують на придатні до додавання версії книг.

Якщо під час приготування книги сталася помилка, замість шляху позиція книги у повернутому списку має бути триелементним кортежем: (початковий шлях, екземпляр виключення, трасування)

startup()[source]

Викликається, коли calibre розпочинає роботу з пристроєм. Виконує усі потрібні дії з ініціалізації. Зауважте, що можна створити декілька екземплярів класу, отже __init__ можна викликати декілька разів, але викликатиме цей метод лише один екземпляр. Цей метод викликається у потоці виконання пристрою, а не у потоці графічного інтерфейсу.

shutdown()[source]

Викликається, коли calibre завершує роботу за наказом користувача або під час приготувань до перезапуску. Виконує правильне завершення усіх завдань. Цей метод викликається у потоці обробки пристрою, а не у потоці роботи графічного інтерфейсу користувача.

get_device_uid()[source]

Має повертати унікальний ідентифікатор для поточного з’єднаного пристрою (викликається одразу після успішного виклику open()). Вам слід реалізувати цей метод, якщо ви встановили ASK_TO_ALLOW_CONNECT = True

ignore_connected_device(uid)[source]

Програма має у майбутньому ігнорувати пристрій, вказаний за uid (результат виклику get_device_uid()). Вам слід реалізувати цей метод, якщо ви встановлюєте ASK_TO_ALLOW_CONNECT = True. Зауважте, що ця функція викликається негайно після open(). Отже, якщо open() кешовано якийсь стан, драйвер має скинути систему до цього стану.

get_user_blacklisted_devices()[source]

Повертає відображення uid пристрою на зручну для користування назву для усіх пристроїв, які користувач наказав ігнорувати.

set_user_blacklisted_devices(devices)[source]

Встановлює список uid пристроїв, які мають ігноруватися цим драйвером.

specialize_global_preferences(device_prefs)[source]

Реалізуйте цей метод, якщо ваш пристрій має перевизначати певне налаштування. Вам слід переконатися, що усі виклики, яким потрібно налаштування, яке може бути перевизначено, використовують device_prefs[„щось“], а не prefs[„щось“]. Ваш метод має викликати device_prefs.set_overrides(налаштування=значення, налаштування=значення, …). У поточній версії використовується для керування метаданими (prefs[„manage_device_metadata“])

set_library_info(library_name, library_uuid, field_metadata)[source]

Реалізуйте цей метод, якщо вам потрібна інформація щодо поточної бібліотеки calibre. Цей метод викликається під час запуску і коли бібліотека calibre змінюється під час з’єднання з пристроєм.

is_dynamically_controllable()[source]

Викликається засобом керування пристроями під час запуску додатків. Якщо цей метод повертає рядок, то: а) у ньому передбачено підтримку інтерфейсу динамічного керування засобу керування пристроями; і б) цю назву слід використовувати під час обміну даними з додатком.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

start_plugin()[source]

Цей метод викликається для запуску додатка. Додаток має розпочати приймання з’єднань зі пристроєм, у який би спосіб він це не робив. Якщо додаток вже приймає з’єднання, ніяких додаткових дій не виконується.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

stop_plugin()[source]

Цей метод викликається для завершення роботи додатка. Після цього додаток має припинити приймання з’єднань і почистити свої сліди у системі. Ймовірно, цей метод має викликати вимикання. Якщо додаток вже не приймає з’єднань, ніяких додаткових дій не виконується.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

get_option(opt_string, default=None)[source]

Повертає значення параметра, вказаного за допомогою opt_string. Цей метод можна викликати, якщо додаток не запущено. Повертає None, якщо вказаного параметра не існує.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

set_option(opt_string, opt_value)[source]

Встановлює значення параметра, вказаного за допомогою opt_string. Цей метод можна викликати, якщо додаток не запущено.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

is_running()[source]

Повертає True, якщо додаток запущено. Якщо додаток не запущено, повертає false.

Цей метод можна викликати у потоці обробки графічного інтерфейсу. Драйвер, який реалізує цей метод, має бути захищено для роботи у потоці.

synchronize_with_db(db, book_id, book_metadata, first_call)[source]

Викликається під час встановлення відповідності книг, коли програма намагається пов’язати книгу на пристрої із книгою у базі даних calibre. Цей метод відповідальний за синхронізацію даних з пристрою із базою даних calibre (якщо це потрібно).

Цей метод має повертати двоелементний кортеж. Першим значенням є набір ідентифікаторів книг calibre, які змінюються, якщо було внесено зміни до бази даних calibre, або None, якщо до бази даних не було внесено змін. Якщо першим значенням є порожня множина, метадані книги на пристрої оновлено на основі метаданих calibre і повернуто на пристрій, але оновлення даних у графічному інтерфейсі для цієї книги не відбулося. Корисно, якщо дані у calibre є правильними, але їх ще слід надіслати на пристрій для читання книг.

Другим значенням є двоелементний кортеж. Перше значення у цьому кортежі визначає, чи слід надсилати формат книги на пристрій. Це значення призначено для перевірки того, що книга на пристрої збігається із книгою у calibre. Це значення має дорівнювати None, якщо немає книг для надсилання, інакше повертає базову назву файла на пристрої (рядок, подібний до щосьтам.epub). Переконайтеся, що до назви файла включено суфікс. Підсистема пристроїв побудує завдання send_books для усіх книг, для яких повернуто значення, що не дорівнюють None. Зауваження: окрім випадку неотримання суфікса назви, назву буде проігноровано тоді, коли на пристрої використовується шаблон для створення назв файлів (такий шаблон використовується на більшості пристроїв). Друге значення у повернутому кортежі позначає, чи є дата для формату майбутньою. Повертає True, якщо це так, інакше повертає False. Calibre показуватиме користувачеві діалогове вікно зі списком усіх книг із датами у майбутньому.

Надзвичайно важливо: цей метод викликається у потоці обробки графічного інтерфейсу. Він має бути безпечним до роботи у потоках щодо потоку засобу керування пристроями.

book_id: ідентифікатор книги у базі даних calibre. book_metadata: об’єкт Metadata для книги, що походить з пристрою. first_call: має значення True, якщо це перший виклик під час синхронізації; False, якщо цей виклик не є першим.

class calibre.devices.interface.BookList(oncard, prefix, settings)[source]

Основа: list

Список книг. Кожен об’єкт Book повинен мати такі поля

1. title

2. authors

3. size (розмір файла книги)

4. datetime (кортеж даних часу для часового поясу UTC)

5. path (шлях до файла книги на пристрої)

6. thumbnail: (може мати значення None) — мініатюра: або рядковий/байтовий об’єкт із даними зображення, або повинен містити атрибут image_path, у якому зберігається абсолютний шлях (у форматі програмної платформи) до зображення.

7. tags (список рядків, може бути порожнім).

supports_collections()[source]

Повертає True, якщо на пристрої передбачено підтримку збірок для цього списку книг.

add_book(book, replace_metadata)[source]

Додає книгу до списку книг. Призначено для збереження усіх внутрішніх для пристрою метаданих. Повертає True, якщо списки книг слід синхронізувати.

remove_book(book)[source]

Вилучає книгу зі списку книг. Одночасно виправляє усі метадані пристрою.

get_collections(collection_attributes)[source]

Повертає словник збірок, створених з collection_attributes. Кожен запис у словнику має формат «назва збірки:[список книг]»

Список книг упорядковується за назвами, окрім збірок, які створено на основі циклів, у яких для упорядковування використовується series_index.

Параметри:

collection_attributes – Список атрибутів об’єкта Book

USB-пристрої великого об’єму

Базовим класом для таких пристроїв є calibre.devices.usbms.driver.USBMS. Цей клас, своєю чергою, успадковує частину своїх функціональних можливостей від своєї основ, документованої нижче. Типовий базовий драйвер на основі USBMS має такий код:

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]

Основи: DeviceConfig, DevicePlugin

Цей клас містить логіку, яка є загальною для усіх драйверів для пристроїв, які експортують себе як пристрої сховищ даних з інтерфейсом USB. Надає реалізації методів для монтування та вилучення пристроїв USBMS для усіх платформ.

VENDOR_ID = 0

VENDOR_ID може бути або цілим числом, або списком цілих чисел, або словником. Якщо це словник, він має бути словником словників у такій формі:

{
integer_vendor_id : { product_id : [list of BCDs], ... },
...
}

PRODUCT_ID = 0

Ціле число або список цілих чисел

BCD = None

BCD може бути або None для розрізнення між пристроями, які засновано на BCD, або списком номерів BCD усіх пристроїв, підтримку яких передбачено у цьому драйвері.

WINDOWS_MAIN_MEM = None

Рядок, який ідентифікує основну пам’ять пристрою серед рядків ідентифікаторів PnP у Windows. Може мати такі значення: None, рядок, список рядків або оброблений формальний вираз.

WINDOWS_CARD_A_MEM = None

Рядок, який ідентифікує першу картку пристрою серед рядків ідентифікаторів PnP у Windows. Може мати такі значення: None, рядок, список рядків або оброблений формальний вираз.

WINDOWS_CARD_B_MEM = None

Рядок, який ідентифікує другу картку пристрою серед рядків ідентифікаторів PnP у Windows. Може мати такі значення: None, рядок, список рядків або оброблений формальний вираз.

OSX_MAIN_MEM_VOL_PAT = None

Використовується засобом виявлення нових драйверів для розрізнення основної пам’яті і карток пам’яті пристрою. Має бути формальним виразом, який відповідає точні монтування основної пам’яті, яка призначається macOS.

BACKLOADING_ERROR_MESSAGE = None

MAX_PATH_LEN = 250

Максимальна довжина шляхів, які створюються на пристрої

NEWS_IN_FOLDER = True

Записує новини у власну теку новин.

reset(key='-1', log_packets=False, report_progress=None, detected_device=None)[source]

Параметри:

- key – Ключ для розблокування пристрою

- log_packets – Якщо true, потік пакетів на пристрій і з пристрою записується до журналу.

- report_progress – Функція, яка викликається із відсотком виконання (числом від 0 до 100) для виконання різних завдань. Якщо викликано з -1, програма вважатиме, що завдання не має даних щодо поступу виконання.

- detected_device – Дані щодо пристрою від засобу сканування пристроїв

set_progress_reporter(report_progress)[source]

Встановлює функцію для надання даних щодо поступу виконання.

Параметри:

report_progress – Функція, яка викликається із відсотком виконання (числом від 0 до 100) для виконання різних завдань. Якщо викликано з -1, програма вважатиме, що завдання не має даних щодо поступу виконання.

card_prefix(end_session=True)[source]

Повертає двоелементний список префіксів до шляхів на картках. Якщо картки немає, для префікса картки буде встановлено значення None. Приклади: („/місце“, „/місце2“) (None, „місце2“) („місце“, None) (None, None)

total_space(end_session=True)[source]

Отримати загальний об’єм доступного місця на точках монтування:

1. Основна пам’ять

2. Картка пам’яті A

3. Картка пам’яті B

Повертає:

Триелементних список із загальним об’ємом у байтах для (1, 2, 3). Якщо за якоюсь із адрес пристрою немає, має повернути 0.

free_space(end_session=True)[source]

Отримати об’єм вільного місця на точках монтування:

1. Основна пам’ять

2. Картка A

3. Картка B

Повертає:

Триелементних список із вільним об’ємом у байтах для (1, 2, 3). Якщо за якоюсь із адрес пристрою немає, має повернути -1.

windows_sort_drives(drives)[source]

Викликається для розрізнення основної пам’яті пристрою та карток пам’яті для пристроїв, які не розрізняють їх за WINDOWS_CARD_NAME. Приклад: EB600

can_handle_windows(usbdevice, debug=False)[source]

Необов’язковий метод для виконання подальших перевірок для пристрою з метою переконатися, що цей драйвер може з ним працювати. Якщо це не так, має повертати False. Цей метод викликається лише після встановлення відповідності виробника, ідентифікаторів продукту та bcd, отже він може виконувати доволі тривалі за часом перевірки. Типова реалізація завжди повертає True. Цей метод викликається лише у Windows. Див. також can_handle().

Зауважте, що для пристроїв на основі USBMS це метод типово делегується can_handle(). Отже, вам слід лише перевизначити can_handle() у вашому підкласі USBMS.

Параметри:

usbdevice – Пристрій USB (usbdevice) у форматі, який повертає calibre.devices.winusb.scan_usb_devices()

open(connected_device, library_uuid)[source]

Виконати усі специфічні для пристрою дії з ініціалізації. Викликається після виявлення пристрою, але перед усіма іншими функціями, які обмінюються даними з пристроєм. Приклад: для пристроїв, які представляються як сховища даних із інтерфейсом USB, цей метод має відповідати за монтування пристрою або, якщо пристрій змонтовано у автоматичному режимі, за пошук точки монтування. Метод calibre.devices.usbms.device.Device.open() містить реалізацію цієї функції і має слугувати добрим прикладом для реалізації доступу до сховищ даних із інтерфейсом USB.

Цей метод може викликати виключення OpenFeedback для показу повідомлення користувачеві.

Параметри:

- connected_device – Пристрій, який ми намагаємося відкрити. Є кортежем (ідентифікатор виробника, ідентифікатор продукту, назва виробника, серійний номер пристрою). Втім, у деяких пристроїв немає серійного номера, а у Windows доступні лише перші три поля, останнє подається як None.

- library_uuid – UUID поточної бібліотеки calibre. Може мати значення None, якщо бібліотеки немає (наприклад, якщо використано з командного рядка).

eject()[source]

Демонтувати і вилучити запис пристрою з операційної системи. Наявність завдань з графічним інтерфейсом, які продовжують виконувати завдання з пристроєм, при цьому не перевіряється.

ЗАУВАЖЕННЯ: цей метод не можна викликати у тому самому потоці виконання, що і решта методів роботи з пристроями.

post_yank_cleanup()[source]

Викликається, якщо користувач від’єднує пристрій без його вилучення з системи.

sanitize_callback(path)[source]

Зворотний виклик, який дозволяє окремим драйверам пристроїв перевизначати засіб обробки шляхів, що використовується методом create_upload_path().

filename_callback(default, mi)[source]

Зворотний виклик, який надає змогу драйверам змінити типову назву файла, яку встановлено за допомогою create_upload_path().

sanitize_path_components(components)[source]

Виконує будь-яку специфічну для пристрою попередню обробку компонентів шляху для файлів, які буде вивантажено на пристрій.

get_annotations(path_map)[source]

Перетворити path_map на annotation_map файлів, які виявлено на пристрої

add_annotation_to_library(db, db_id, annotation)[source]

Додати анотацію до бібліотеки calibre

class calibre.devices.usbms.cli.CLI[source]

class calibre.devices.usbms.driver.USBMS(plugin_path)[source]

Основи: CLI, Device

Базовий клас для усіх пристроїв USBMS. Реалізує логіку для надсилання, отримання, оновлення, кешування метаданих тощо.

description = 'Обмін даними із пристроєм для читання ел. книг.'

Короткий рядок із описом призначення додатка

author = 'John Schember'

Автор цього додатка

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

booklist_class

alias of BookList

book_class

alias of Book

FORMATS = []

Упорядкований список підтримуваних форматів

CAN_SET_METADATA = []

Визначає, чи може бути встановлено метадані для книг за допомогою графічного інтерфейсу.

get_device_information(end_session=True)[source]

Опитує пристрій щодо даних про нього. Див. L{DeviceInfoQuery}.

Повертає:

(назва пристрою, версія пристрою, версія програмного забезпечення на пристрої, тип MIME) Кортеж даних може містити п’ятий елемент, який є словником із даними щодо диска. Див. приклад у usbms.driver.

set_driveinfo_name(location_code, name)[source]

Встановити назву пристрою у файлі driveinfo у значення «name». Цей параметр не змінюється, аж доки файл не буде повторно створено або його назву не буде знову змінено.

Недискові пристрої мають реалізовувати цей метод на основі кодів розташування, які повертаються методом get_device_information().

books(oncard=None, end_session=True)[source]

Повертає список електронних книг на пристрої.

Параметри:

oncard – Якщо «carda» або «cardb», повертає список електронних книг на вказаній картці пам’яті. Якщо інше значення, повертає список електронних книг у основній пам’яті пристрою. Якщо вказано картку, але на картці немає книг, повертає порожній список.

Повертає:

BookList.

upload_books(files, names, on_card=None, end_session=True, metadata=None)[source]

Вивантажити список книг на пристрій. Якщо якийсь файл вже існує на пристрої, його буде замінено. Цей метод має повідомляти про FreeSpaceError, якщо на пристрої недостатньо вільного місця. Текст FreeSpaceError має містити слово «card», якщо on_card має значення, відмінне від None. Інакше, він має містити слово «memory».

Параметри:

- files – Список шляхів

- names – Список назв файлів, які повинні мати книги після вивантаження на пристрій. len(names) == len(files)

- metadata – Якщо має значення, яке є відмінним від None, є списком об’єктів Metadata. Ідея полягає у використанні метаданих для визначення, куди на пристрої слід зберегти книгу. len(metadata) == len(files). Окрім типового зображення обкладинки (шляху до зображення), може також існувати атрибут мініатюри, який матиме вищий пріоритет. Атрибут мініатюри записується у форматі (ширина, висота, дані обкладинки у форматі jpeg).

Повертає:

Список триелементни кортежів. Список призначено для передавання до add_books_to_metadata().

upload_cover(path, filename, metadata, filepath)[source]

Вивантажує зображення обкладинки книги на пристрій. Типова реалізація не виконує ніяких дій.

Параметри:

- path – Повний шлях до теки, де розташовано відповідну книгу.

- filename – Назва файла книги без суфікса назви.

- metadata – метадані, що стосуються книги. Скористайтеся виразом metadata.thumbnail, якщо вам потрібне зображення обкладинки.

- filepath – Шлях до файла електронної книги повністю.

add_books_to_metadata(locations, metadata, booklists)[source]

Додає адреси до списків книг. Ця функція не повинна обмінюватися даними з пристроєм.

Параметри:

- locations – Результати виклику L{upload_books}

- metadata – Список об’єктів Metadata, той самий, що і для upload_books().

- booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

delete_books(paths, end_session=True)[source]

Вилучити книги за вказаними шляхами на пристрої.

remove_books_from_metadata(paths, booklists)[source]

Вилучити книги зі списку метаданих. Ця функція не повинна обмінюватися даними з пристроєм.

Параметри:

- paths – шляхи до книг на пристрої.

- booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

sync_booklists(booklists, end_session=True)[source]

Оновити метадані на пристрої.

Параметри:

booklists – Кортеж, що містить результати викликів (books(oncard=None)(), books(oncard='carda')(), :meth`books(oncard=“cardb“)`).

classmethod normalize_path(path)[source]

Повертає шлях із природними для платформи роздільниками.

Дії інтерфейсу користувача

Якщо ви додаєте власний додаток у файлі ZIP, вам слід створити підкласи для InterfaceActionBase і для InterfaceAction. Метод load_actual_plugin() вашого підкласу InterfaceActionBase має повертати створений екземпляр об’єкта вашого підкласу InterfaceBase.

class calibre.gui2.actions.InterfaceAction(parent, site_customization)[source]

Основа: QObject

Додаток, що відповідає «дії», яку буде виконано у графічному інтерфейсі користувача. Усі пункти на панелі та у контекстних меню реалізуються за допомогою таких додатків.

Зауважте, що цей клас є базовим класом для цих додатків. Втім, для інтегрування додатка до системи додатків calibre вам слід створити клас-обгортку, який посилатиметься на сам додаток. Див. приклад у модулі calibre.customize.builtins.

Якщо два об’єкта InterfaceAction мають однакові назви, перевага надається об’єкту із вищою пріоритетністю.

Підкласти мають реалізовувати методи :genesis(), library_changed(), location_selected(), shutting_down(), initialization_complete() та tag_browser_context_action().

Після ініціалізації цей додаток матиме доступ до основного графічного інтерфейсу calibre за допомогою учасника класу gui. Ви можете отримувати доступ до інших додатків за назвою. Приклад:

self.gui.iactions['Save To Disk']

Щоб отримати доступ до самого додатка, скористайтеся атрибутом interface_action_base_plugin. Цей атрибут стане доступним, лише після ініціалізації додатка. Корисно, коли вам потрібно скористатися методами з класу додатка, зокрема do_user_config().

QAction, вказана за допомогою action_spec, автоматично створюється і робиться доступною як self.qaction.

name = 'Implement me'

Назва додатка. Якщо існує два додатки із однаковими назвами, перевагу матиме додаток із вищим рівнем пріоритетності.

priority = 1

Пріоритетність додатка. Якщо існує два додатки із однаковими назвами, перевагу матиме додаток із вищим рівнем пріоритетності.

popup_type = 1

Тип контекстної підказки меню, якщо цей додаток додає кнопку на панель інструментів.

auto_repeat = False

Визначає, чи має ця дія повторюватися, якщо утримується натиснутою комбінація клавіш для виклику додатка.

action_spec = ('text', 'icon', None, None)

У такій формі: (текст, шлях до піктограми, підказка, клавіатурне скорочення). Піктограма, підказка і клавіатурне скорочення можуть мати значення None. Клавіатурне скорочення має бути рядком, None або кортежем скорочень. Якщо має значення None, клавіатурне скорочення для дії не реєструватиметься. Якщо передано порожній кортеж, буде зареєстровано скорочення без типової прив’язки до клавіш.

action_shortcut_name = None

Якщо не вказано «None», буде використано для назви, яку буде показано користувачеві при налаштовуванні клавіатурних скорочень для вказаної вище специфікації дії, замість action_spec[0]

action_add_menu = False

Якщо має значення True, автоматично створюється і додається до self.qaction меню.

action_menu_clone_qaction = False

Якщо має значення True, до меню self.qaction додається клон self.qaction. Якщо ви хочете, щоб текст цієї доданої дії був не таким як у self.qaction, встановіть для цієї змінної нове текстове значення.

dont_add_to = frozenset({})

Набір місць, до яких не слід додавати цю дію. Список можливих місць наведено у описі all_locations.

dont_remove_from = frozenset({})

Набір місць, з яких не слід вилучати цю дію. Список можливих місць наведено у описі all_locations.

action_type = 'global'

Тип дії «current» означає, що дія виконується для поточної панелі перегляду. Тип «global» означає дію, яку не призначено для поточної панелі перегляду, а призначено для усього екземпляра calibre.

accepts_drops = False

Якщо має значення True, ця InterfaceAction буде мати змогу обробляти події з перетягування зі скиданням. Докладніше про це у описі методів accept_enter_event(), :meth`:accept_drag_move_event`, drop_event().

accept_enter_event(event, mime_data)[source]

Цей метод має повертати True тоді і лише тоді, коли ця дія інтерфейсу здатна обробляти подію з перетягування. Не викликайте accept/ignore для події, оскільки ці виклики буде оброблено інтерфейсом користувача calibre.

accept_drag_move_event(event, mime_data)[source]

Цей метод має повертати True тоді і лише тоді, коли ця дія інтерфейсу здатна обробляти подію з перетягування. Не викликайте accept/ignore для події, оскільки ці виклики буде оброблено інтерфейсом користувача calibre.

drop_event(event, mime_data)[source]

Цей метод має виконати певну корисну дію і повернути True тоді і лише тоді, коли ця дія інтерфейсу здатна обробляти подію скидання. Не викликайте accept/ignore для події, про це подбає інтерфейс користувача calibre. Вам не слід виконувати блокування або тривалі дії у цій функції. Замість цього надішліть сигнал або скористайтеся QTimer.singleShot і швидко поверніть керування. Приклади можна знайти серед вбудованих до програми дій.

create_menu_action(menu, unique_name, text, icon=None, shortcut=None, description=None, triggered=None, shortcut_name=None, persist_shortcut=False)[source]

Зручний метод для спрощення додавання пунктів дій до QMenu. Повертає створену QAction. Ця дія має лише один додатковий атрибут, calibre_shortcut_unique_name, який, якщо не має значення None, посилається на унікальну назву, під якою цю дію зареєстровано у засобі керування клавіатурними скороченнями.

Параметри:

- menu – QMenu, до якого буде додано пункт створеної дії.

- unique_name – Унікальна назва для цієї дії. Має бути унікальною на загальному рівні, тому робіть її якомога описовішою. Якщо маєте сумніви, додайте до неї UUID.

- text – Текст дії.

- icon – QIcon або назва файла. Назву файла буде передано до вбудованої функції QIcon.ic(), отже вам не потрібно передавати повний шлях до теки images.

- shortcut – Рядок, список рядків, None або False. Якщо False, для цієї дії не буде зареєстровано клавіатурного скорочення. Якщо None, буде зареєстровано клавіатурне скорочення без типового значення. Рядок або список рядків реєструють клавіатурне скорочення із вказаними типовим значеннями.

- description – Опис дії. Використовується для встановлення підказок.

- triggered – Придатна до виклику функція, яка з’єднується із сигналом-перемикачем створеної дії.

- shortcut_name – Текстове повідомлення, яке буде показано користувачеві під час налаштовування клавіатурних скорочень для цієї дії. Типово встановлено значення text.

- persist_shortcut – Скорочення для дій, які не є універсальними або залежать від бібліотек, можуть зникати при редагуванні інших клавіатурних скорочень, якщо для параметра `persist_shortcut` не встановлено значення True.

load_resources(names)[source]

Якщо цей додаток постачається у форматі файла ZIP (додаток, що встановлюється користувачем), цей метод дозволить вам завантажити ресурси з файла ZIP.

Наприклад, для завантаження зображення:

pixmap = QPixmap()
pixmap.loadFromData(tuple(self.load_resources(['images/icon.png']).values())[0])
icon = QIcon(pixmap)

Параметри:

names – Список шляхів до ресурсів у файлі ZIP з використанням роздільника /

Повертає:

Словник у формі {назва: вміст_файла}. Усі назви, які не було знайдено у файлі ZIP, не буде включено до словника.

genesis()[source]

Налаштовує цей додаток. Викликається лише раз під час ініціалізації. Доступним є self.gui. Дія, вказана action_spec є доступною як self.qaction.

location_selected(loc)[source]

Викликається щоразу, коли змінюється список книг, що показується у calibre. У поточній версії можливими значенням loc є такі значення: library, main, card і cardb.

Цей метод має вмикати або вимикати цю дію і її підлеглі дії відповідним чином для цього місця.

library_about_to_change(olddb, db)[source]

Викликається у відповідь на зміну поточної бібліотеки.

Параметри:

- olddb – LibraryDatabase, що відповідає попередній бібліотеці.

- db – LibraryDatabase, що відповідає новій бібліотеці.

library_changed(db)[source]

Викликається у відповідь на зміну поточної бібліотеки.

Параметри:

db – LibraryDatabase, що відповідає поточній бібліотеці.

gui_layout_complete()[source]

Викликається один раз для кожної дії після завершення компонування основного графічного інтерфейсу. Якщо вашій дії потрібно внести зміни до компонування, їх слід внести тут, а не у initialization_complete().

initialization_complete()[source]

Викликається один раз на кожну дію, коли завершується ініціалізація основного графічного інтерфейсу.

tag_browser_context_action(index)[source]

Викликається при показі контекстного меню навігатора мітками. index є значенням QModelIndex, яке вказує на запис навігатора мітками, на якому відбулося клацання правою кнопкою миші. Перевірити його на чинність можна за допомогою index.valid(), а отримати відповідний об’єкт TagTreeItem можна за допомогою index.data(Qt.ItemDataRole.UserRole). Усі пункти дій, які є результатом роботи цього методу, буде додано до контекстного меню.

shutting_down()[source]

Викликається один раз для кожного з додатків, коли основний графічний інтерфейс програми перебуває у стані завершення роботи. Має звільнити усі ресурси, але не намагатися блокувати процес завершення роботи на довгі проміжки часу.

class calibre.customize.InterfaceActionBase(*args, **kwargs)[source]

Основа: Plugin

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Автор цього додатка

type = 'Поведінка інтерфейсу користувача'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

can_be_disabled = False

Якщо матиме значення False, користувач не зможе вимкнути цей додаток. Користуйтеся із осторогою.

load_actual_plugin(gui)[source]

Цей метод має повертати поточний інтерфейс об’єкта додатка дії.

Додатки налаштувань
class calibre.customize.PreferencesPlugin(plugin_path)[source]

Основа: Plugin

Додаток, що забезпечує роботу віджета, який показується у діалоговому вікні «Налаштування».

У цього додатка є лише один важливий метод, create_widget(). Різноманітні поля додатка керують тим, як його буде категоризовано у інтерфейсі користувача.

supported_platforms = ['windows', 'osx', 'linux']

Список платформ, на яких працює додаток. Приклад: ['windows', 'osx', 'linux']

author = 'Kovid Goyal'

Автор цього додатка

type = 'Налаштування'

Тип цього додатка. Використовується для розподілу додатків за категоріями у графічному інтерфейсі програми

can_be_disabled = False

Якщо матиме значення False, користувач не зможе вимкнути цей додаток. Користуйтеся із осторогою.

config_widget = None

Шлях імпортування модуля, який містить клас із назвою ConfigWidget, який реалізує ConfigWidgetInterface. Використовується методом create_widget().

category_order = 100

Визначає, де має бути у списку категорій категорія цього додатка.

name_order = 100

Визначає, де у списку назв у категорії має бути gui_name цього додатка.

category = None

Категорія, до якої належить цей додаток.

gui_category = None

Назва категорії, яку буде показано користувачеві для цього додатка.

gui_name = None

Назва, яку буде показано користувачеві цього додатка.

icon = None

Піктограма цього додатка, має бути вказано абсолютний шлях.

description = None

Опис, який використовується для підказок та інших довідкових панелей.

create_widget(parent=None)[source]

Створює і повертає сам віджет Qt, який використовується для встановлення цієї групи налаштувань. Віджет має реалізовувати calibre.gui2.preferences.ConfigWidgetInterface.

Типова реалізація використовує config_widget для створення екземпляра віджета.

class calibre.gui2.preferences.ConfigWidgetInterface[source]

Цей клас визначає інтерфейс, який мають реалізовувати усі віджети, які показано у діалоговому вікні «Налаштування». Базовим класом, який реалізовує цей інтерфейс і визначає зручні методи, є ConfigWidgetBase.

changed_signal = None

Цей сигнал має бути надіслано кожного разу, коли користувач змінює значення у цьому віджеті.

supports_restoring_to_defaults = True

Встановіть значення True тоді і лише тоді, коли реалізовано метод restore_to_defaults().

restore_defaults_desc = 'Відновити типові значення параметрів. Щоб зберегти типові параметри, вам слід натиснути кнопку «Застосувати».'

Підказка для кнопки «Відновити типові значення»

restart_critical = False

Якщо має значення True, діалогове вікно «Налаштування» заборонить користувачеві продовжувати зміну налаштувань. Працює, лише якщо commit() повертає True.

genesis(gui)[source]

Викликається один раз перед показом віджета, має виконати усі потрібні налаштовування.

Параметри:

gui – Основний графічний інтерфейс calibre

initialize()[source]

Має встановити усі початкові значення налаштувань (значення, які зберігаються у файлах налаштувань). Інструкція «return» є необов’язковою. Повертає False, якщо вікно не має бути показано.

restore_defaults()[source]

Має встановити типові значення для усіх налаштувань.

commit()[source]

Зберігає усі змінені параметри. Повертає True, якщо зміни потребують перезапуску програми, і False, якщо ні. Надсилає виключення AbortCommit, щоб повідомити про те, що сталася помилка. Ви маєте самі визначити, як слід повідомити користувачеві про помилку і про те, як її виправити.

refresh_gui(gui)[source]

Викликається один раз після надсилання цього віджета. Відповідає за повторне читання графічним інтерфейсом усіх змінених параметрів. Зауважте, що типово графічний інтерфейс попри усе повторно ініціалізує різноманітні елементи, отже для більшості віджетів потреби у використанні цього методу немає.

initial_tab_changed()[source]

Буде викликано, якщо до початково показаної вкладки було внесено зміни до показу віджета, але після його ініціалізації.

class calibre.gui2.preferences.ConfigWidgetBase(parent=None)[source]

Базовий клас, який містить код для спрощення додавання стандартних віджетів налаштовування, зокрема пунктів позначок, спадних списків, текстових полів тощо. Див. метод register().

Цей клас виконує автоматичну обробку сповіщення про зміни, повернення до типових значень, перенесення даних із об’єктів графічного інтерфейсу до об’єктів налаштувань тощо для зареєстрованих параметрів.

Якщо ваш віджет налаштувань успадковується від цього класу, але включає параметр, який не зареєстровано, вам слід перевизначити методи ConfigWidgetInterface і викликати методи базового класу всередині перевизначення.

changed_signal

Цей сигнал має бути надіслано кожного разу, коли користувач змінює значення у цьому віджеті.

supports_restoring_to_defaults = True

Встановіть значення True тоді і лише тоді, коли реалізовано метод restore_to_defaults().

restart_critical = False

Якщо має значення True, діалогове вікно «Налаштування» заборонить користувачеві продовжувати зміну налаштувань. Працює, лише якщо commit() повертає 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]

Зареєструвати параметр.

Параметри:

- name – Назва параметра

- config_obj – Об’єкт налаштувань, який читає або запису параметр.

- gui_name – Назва об’єкта графічного інтерфейсу користувача, який надає інтерфейс для зміни параметра. Типовою назвою вважається 'opt_' + назва.

- choices – Якщо цей параметр є параметром із декількома варіантами (визначається спадним списком), список варіантів. Форматом запису списку є двоелементний кортеж. Формат списку: [(назва у графічному інтерфейсі, значення), ...]

- setting – Клас, що відповідає за керування цим параметром. Типовий клас обробляє майже усі класи, отже цей параметр використовується нечасто.

initialize()[source]

Має встановити усі початкові значення налаштувань (значення, які зберігаються у файлах налаштувань). Інструкція «return» є необов’язковою. Повертає False, якщо вікно не має бути показано.

commit(*args)[source]

Зберігає усі змінені параметри. Повертає True, якщо зміни потребують перезапуску програми, і False, якщо ні. Надсилає виключення AbortCommit, щоб повідомити про те, що сталася помилка. Ви маєте самі визначити, як слід повідомити користувачеві про помилку і про те, як її виправити.

restore_defaults(*args)[source]

Має встановити типові значення для усіх налаштувань.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка

Інтерфейс командного рядка

Примітка

У macOS засоби керування за допомогою командного рядка є частиною пакунка calibre. Наприклад, якщо calibre встановлено до /Applications, засоби керування за допомогою командного рядка зберігатимуться у каталозі /Applications/calibre.app/Contents/console.app/Contents/MacOS/. Тому, наприклад, щоб запустити ebook-convert, вам слід скористатися такою командою: /Applications/calibre.app/Contents/MacOS/ebook-convert.

Документовані команди

- 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

Недокументовані команди

- ebook-device

- markdown-calibre

Довідку щодо використання недокументованих команд можна отримати у відповідь на віддання відповідної команди без аргументів у терміналі.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibre

calibre

calibre [параметри] [шлях_до_книги або адреса calibre ...]

Запустити головний графічний інтерфейс calibre і, якщо вказано, додати книгу, зазначену як шлях_до_книги до бази даних. Ви можете також вказувати адреси calibre для виконання різноманітних дій, відмінних від простого додавання книг. Приклад:

calibre://view-book/test_library/1842/epub

У відповідь на цю команду у засобі перегляду буде відкрито книгу із ідентифікатором 1842 у форматі EPUB з бібліотеки
«test_library». Назви бібліотек збігаються із назвами тек цих бібліотек, але пробіли має бути замінено на символи підкреслювання. Повний опис різноманітних дій на основі адрес наведено у підручнику користувача.

Якщо ви передаєте аргументи calibre, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--detach

Від’єднати від контрольного термінала, якщо такий був (лише Linux)

--help, -h

показати це довідкове повідомлення і завершити роботу

--ignore-plugins

Ігнорувати нетипові додатки. Корисно, якщо вами встановлено додаток, використання якого не дає запустити calibre.

--no-update-check

Не перевіряти наявність поновлень

--shutdown-running-calibre, -s

Завершує роботу запущеного екземпляра calibre, якщо такий існує. Зауважте, що якщо програма виконує завдання, виконання цих завдань буде перервано без додаткових повідомлень, отже будьте обережні з цією командою.

--start-in-tray

Запускати згорнутою у лоток.

--verbose, -v

Ігнорується, не використовуйте. Залишено лише з міркувань сумісності.

--version

показати дані щодо версії програми і завершити роботу

--with-library

Використовувати бібліотеку, розташовану у вказаному каталозі.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibre-customize

calibre-customize

Параметри calibre-customize

Налаштуйте Calibre, використовуючи зовнішні додатки.

Якщо ви передаєте аргументи calibre-customize, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--add-plugin, -a

Додайте додаток, вказавши розташування файла ZIP, який його містить.

--build-plugin, -b

Для розробників додатків: шлях до теки, у якій зберігається код додатка. За допомогою цієї команди можна створити архів zip з додатком і оновити його у calibre у автоматичному режимі.

--customize-plugin

Налаштувати додаток. Вкажіть назву додатка і рядок налаштовування, відокремлені комою. Рядок налаштовування є тим самим рядком, який би ви ввели при налаштовуванні додатка у основному графічному інтерфейсі calibre.

--disable-plugin

Вимкнути вказаний за назвою додаток

--enable-plugin

Увімкнути вказаний за назвою додаток

--help, -h

показати це довідкове повідомлення і завершити роботу

--list-plugins, -l

Список всіх встановлених додатків

--remove-plugin, -r

Вилучити нетиповий додаток за назвою. Не впливає на вбудовані додатки.

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibre-debug

calibre-debug

calibre-debug [параметри]

З метою діагностики роботи calibre можна скористатися різними інтерфейсами командного рядка. Якщо виконати цю команду без параметрів, буде запущено вбудований інтерпретатор python. Крім того, ви можете запустити у режимі діагностики основний графічний інтерфейс calibre, переглядач calibre та редактор calibre.

Передбачено також інтерфейси для різноманітних частин calibre, у яких немає відповідних інструментів командного рядка, зокрема засобу обрізання шрифтів, засобу порівняння електронних книг тощо.

Ви також можете скористатися calibre-debug для запуску окремих скриптів. Зробити це можна так:

calibre-debug -e myscript.py -- --option1 --option2 file1 file2 …

Скрипту буде передано усі дані після –. Ви також можете скористатися calibre-debug як вступною частиною у скриптах, ось так:

#!/usr/bin/env -S calibre-debug -e -- –

Якщо ви передаєте аргументи calibre-debug, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--add-simple-plugin

Додати простий додаток (тобто додаток, що складається лише з файла .py) можна за допомогою визначення шляху до файла py, який містить код додатка.

--command, -c

Виконати код мовою програмування Python.

--debug-device-driver, -d

Діагностика виявлення пристроїв

--default-programs

Зареєструвати (скасувати реєстрацію) calibre серед типових програм Windows. --default-programs = (register|unregister)

--diff

Запустити засіб пошуку відмінностей calibre. Приклад: calibre-debug --diff файл1 файл2

--edit-book

Запустити засіб редагування книг calibre у режимі діагностики.

--exec-file, -e

Виконати код Python у файлі.

--explode-book, -x

Розпакувати книгу до вказаної теки. Користування: -x файл.epub тека_виведення Експортує книгу як збірку файлів та метаданих HTML, які ви зможете редагувати за допомогою стандартних інструментів редагування HTML. Працює з файлами EPUB, AZW3, HTMLZ і DOCX.

--export-all-calibre-data

Експортувати усі дані calibre (книги, параметри, додатки). Зазвичай, програма попросить вас вказати теку і бібліотеки для експортування. Ви також можете вказати їх як аргументи командного рядка, щоб пропустити питання. Теку експортування і бібліотеки слід вказувати у форматі абсолютних шляхів. Для експортування усіх бібліотеки можна скористатися спеціальним ключовим слоовом «all». Приклади: calibre-debug --export-all-calibre-data # інтерактивне користування calibre-debug --export-all-calibre-data /шлях/до/порожньої/теки/експортування /шлях/до/теки/бібліотеки1 /шлях/до/бібліотеки2 calibre-debug --export-all-calibre-data /тека/експортування all # експортувати усі відомі бібліотеки

--fix-multiprocessing

Для внутрішнього використання

--gui, -g

Запустити графічний інтерфейс з увімкненою діагностикою. Дані діагностики буде виведено до stdout та stderr.

--gui-debug

Запустити графічний інтерфейс з консоллю діагностики, записавши журнал до вказаного каталогу. Призначено лише для внутрішнього використання. Скористайтеся параметром -g для відкриття графічного інтерфейсу у режимі діагностики.

--help, -h

показати це довідкове повідомлення і завершити роботу

--implode-book, -i

Запакувати раніше розпаковану книгу. Користування: -i каталог_виведення файл.epub Імпортує книгу з файлів у каталозі каталог_виведення. Такі файли має бути попередньо створено за допомогою виклику команди із параметром --explode-book. Не забудьте вказати саме той тип файла, який було використано для розпаковування книги.

--import-calibre-data

Імпортувати раніше експортовані дані calibre

--inspect-mobi, -m

Перевірити файли MOBI за вказаними шляхами.

--paths

Вивести записи шляхів, необхідних для налаштовування середовища calibre.

--run-plugin, -r

Запустити додаток інтерфейсу командного рядка. Приклад: calibre-debug -r "Назва додатка" -- файл1 --option1 Всі дані після -- буде передано додатку як параметри.

--run-test, -t

Запустити вказані перевірки. Скористайтеся особливим словом «all», щоб наказати виконати усі перевірки. Якщо назва перевірки починається з крапки, програма припускатиме, що вказано назву модуля. Якщо назва перевірки починається з @, програма вважатиме її назвою категорії.

--run-without-debug

Не запускати із встановленим прапорцем DEBUG

--shutdown-running-calibre, -s

Завершує роботу запущеного екземпляра calibre, якщо такий існує. Зауважте, що якщо програма виконує завдання, виконання цих завдань буде перервано без додаткових повідомлень, отже будьте обережні з цією командою.

--subset-font, -f

Підмножина символів вказаного шрифту. Додайте -- після цього параметра, щоб передати параметр програмі створення підмножин символів.

--test-build

Перевірити бінарні модулі у збірці.

--version

показати дані щодо версії програми і завершити роботу

--viewer, -w

Запустити програму для перегляду електронних книг у режимі діагностики

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibre-server

calibre-server

calibre-server [параметри] [шлях до теки бібліотеки...]

Запустити сервер обробки даних calibre. Сервер обробки даних calibre надаватиме змогу працювати із бібліотеками calibre з інтернету. Ви можете передати шлях до тек бібліотеки у аргументах команди calibre-server.
Якщо вами не буде вказано шляхів, буде використано усі бібліотеки, про які відомо основній програмі calibre.

Якщо ви передаєте аргументи calibre-server, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--access-log

Шлях до файла журналу доступу. У цьому журналі містяться дані щодо клієнтів, які встановлювали з’єднання із сервером, та виконаних запитів. Типово, запис до цього журналу не виконується.

--ajax-timeout

Час очікування на відповідь сервера у секундах.

--auth-mode

Виберіть тип розпізнавання. Встановити режим розпізнавання HTTP, який використовуватиметься на сервері. Встановіть значення «basic», якщо сервер перебуває за проксі з SSL. Якщо це не так, лишіть значення «auto», за якого використовуватиметься «basic», якщо налаштовано SSL, і «digest», якщо його не налаштовано.

--auto-reload

Автоматично перезавантажувати сервер, якщо змінюється початковий код. Корисно для розробників. Вам також слід вказати невеличке значення для часу очікування на завершення роботи перед перезавантаженням.

--ban-after

Кількість невдалих спроб для блокування. Кількість невдалих спроб увійти, після виконання яких IP-адресу буде заблоковано

--ban-for

Заблокувати IP-адреси, за якими не вдається увійти. Тимчасово заблокувати доступ для IP-адрес, за якими не вдається повторно увійти протягом вказаної кількості хвилин. Корисно для запобігання спробам підібрати пароль. Якщо встановлено значення 0, блокування не виконуватиметься.

--book-list-mode

Виберіть типовий режим списку книг. Встановіть типовий режим списку книг, який буде використано для нових користувачів. Окремі користувачі можуть перевизначати цей типовий режим у власних параметрах роботи програми. Типовим є режим таблиці обкладинок.

--compress-min-size

Мінімальний розмір для використання стискання даних у відповідях (у байтах).

--custom-list-template

Шлях до файла JSON, у якому містяться дані шаблона для режиму нетипового списку книг. Найпростішим способом створення такого файла шаблона є використання пункту «Налаштування -> Обмін мережею -> Шаблон списку книг» у calibre, створення шаблона та його експортування.

--daemonize

Запустити процес у тлі як фонову службу (лише Linux).

--displayed-fields

Обмежити перелік показаних полів, які визначено користувачем. Список відокремлених комами визначених користувачем полів метаданих, які буде показано сервером вмісту у переглядах /opds та /mobile. Якщо ви вкажете цей параметр, усі поля поза цим списком не буде показано. Приклад: my_rating,my_tags

--enable-allow-socket-preallocation, --disable-allow-socket-preallocation

Попереднє резервування сокетів, наприклад для активації сокетів у systemd. Типово, цей параметр увімкнено.

--enable-auth, --disable-auth

Розпізнавання за допомогою пароля для доступу до сервера. Зазвичай, доступ до сервера не обмежено: отримати його може будь-хто. За допомогою цього параметра ви можете обмежити доступ переліком попередньо визначних користувачів. Типово, цей параметр вимкнено.

--enable-fallback-to-detected-interface, --disable-fallback-to-detected-interface

Повернення на автовизначений інтерфейс. Якщо з якоїсь причини сервер не зможе прив’язатися до інтерфейсу, вказаного за допомогою параметра listen_on, він спробує визначити інтерфейс, за допомогою якого комп’ютер з’єднується з іншими комп’ютерами, і прив’язатися до нього. Типово, цей параметр увімкнено.

--enable-local-write, --disable-local-write

Дозволити локальні з’єднання для внесення змін без розпізнавання. Зазвичай, якщо розпізнавання не увімкнено, сервер працюватиму у режимі лише читання, так, щоб забороняти анонімним користувачам вносити зміни до ваших бібліотек calibre. За допомогою цього пункту можна дозволити вносити зміни будь-кому, хто встановлює з’єднання з того комп’ютера, на якому працює сервер. Це корисно, якщо ви хочете, щоб сервер працював без розпізнавання, але не хочете втрачати можливості вносити зміни до ваших бібліотек calibre за допомогою calibredb. Зауважте, що вмикання цього параметра означає, що будь-яка програму, запущена на відповідному комп’ютері, зможе вносити зміни до ваших бібліотек calibre. Типово, цей параметр вимкнено.

--enable-log-not-found, --disable-log-not-found

Записувати до журналу запити з HTTP 404 (Не знайдено). Зазвичай, до журналу сервера записуються усі запити HTTP для ресурсів, які не було знайдено. Це може призвести до значного розростання журналу, якщо ваш сервер атакуватимуть боти. За допомогою цього параметра можна вимкнути запис до журналу відповідних повідомлень. Типово, цей параметр увімкнено.

--enable-use-bonjour, --disable-use-bonjour

Оголошувати подачі OPDS за допомогою BonJour. Оголошувати про подачі OPDS за допомогою служби BonJour, щоб засновані на OPDS програми для читання могли виявляти сервер і автоматично з ним з’єднуватися. Типово, цей параметр увімкнено.

--enable-use-sendfile, --disable-use-sendfile

Нульові передавання файлів при копіюванні для підвищення швидкодії. За допомогою цього можна скористатися передаваннями нульового копіювання у ядрі під час надсилання файлів мережею і підвищити швидкодію. Втім, використання цього способу може призвести до пошкодження файлів у деяких файлових системах із помилками. Якщо у вас виникають проблеми із пошкодженням файлів, вимкніть цю можливість. Типово, цей параметр увімкнено.

--help, -h

показати це довідкове повідомлення і завершити роботу

--ignored-fields

Ігноровані визначені користувачем поля метаданих. Список полів метаданих, визначених користувачем, які не буде показано сервером вмісту у переглядах /opds та /mobile. Приклад: my_rating,my_tags

--listen-on

Інтерфейс, на якому слід очікувати на з’єднання. Типово, програма очікує на з’єднання на усіх інтерфейсах IPv6 та IPv4. Ви можете змінити цю поведінку: наприклад, визначити адресу «127.0.0.1», щоб програма очікувала на з’єднання IPv4 лише на комп’ютері, на якому запущено сервер, або вказати «0.0.0.0», щоб сервер очікував на всі вхідні з’єднання IPv4.

--log

Шлях до файла журналу сервера. У цьому журналі зберігаються дані і помилки сервера, а не повідомлення щодо доступу до сервера. Типово, дані журналу записуються до stdout.

--manage-users

Керування базою даних користувачів, які можуть встановлювати з'єднання із цим сервером. Можете користуватися автоматичним режимом, додавши до команди –. Див. calibre-server --manage-users -- help, щоб дізнатися більше. Див. також параметр --userdb.

--max-header-line-size

Макс. розмір одного заголовка HTTP (у кБ).

--max-job-time

Максимальна тривалість процесів обробки. Максимальна тривалість процесів обробки (у хвилинах). Встановіть нульове значення, якщо тривалість не слід обмежувати.

--max-jobs

Максимальна кількість процесів обробки. Процеси обробника запускаються за потреби і використовуються для виконання складних завдань, зокрема приготування книги до перегляду, перетворення даних у інший формат. Зазвичай, максимальна кількість процесів таких обробників визначається за кількістю ядер процесора комп’ютера. Ви можете змінити типове значення.

--max-log-size

Макс. розмір файла журналу (у МБ). Максимальний розмір файлів журналу, які створюватимуться сервером. Щойно розмір файла журналу перевищить вказане значення, відбудеться автоматична ротація системи файлів журналу. Встановіть нульове значення, щоб вимкнути ротацію журналу.

--max-opds-items

Максимальна кількість книг у подачах OPDS. Максимальна кількість книг, дані яких сервер повертатиме у одній подачі OPDS.

--max-opds-ungrouped-items

Максимальна кількість незгрупованих записів у подачах OPDS. Групувати записи у категоріях, зокрема іменами авторів та мітками, за першими літерами, якщо літері відповідає кількість записів, більша за вказану. Встановіть нульове значення, щоб вимкнути.

--max-request-body-size

Макс. дозволений розмір файлів для вивантаження на сервер (у МБ).

--num-per-page

Кількість книг, які може бути показано на одній сторінці. Кількість книг, які може бути показано на одній сторінці у засобі навігації.

--pidfile

Записати ідентифікатор процесу до вказаного файла

--port

Порт, на якому слід очікувати на з’єднання.

--search-the-net-urls

Шлях до файла JSON, у якому містяться адреси для функціональної можливості «Шукати у інтернеті». Найпростішим способом створення такого файла є перехід на сторінку «Налаштування -> Обмін мережею -> Шукати у інтернеті» у головному вікні calibre, створення адрес і їх експортування.

--shutdown-timeout

Загальний час очікування на коректне завершення у секундах.

--ssl-certfile

Шлях до файла сертифіката SSL.

--ssl-keyfile

Шлях до файла закритого ключа SSL.

--timeout

Максимальний час (у секундах) підтримання бездіяльного з’єднання перед розірванням.

--trusted-ips

Дозволити з’єднання із вказаних IP-адрес для внесення змін без розпізнавання. Зазвичай, якщо розпізнавання не увімкнено, сервер працюватиму у режимі лише читання, так, щоб забороняти анонімним користувачам вносити зміни до ваших бібліотек calibre. За допомогою цього пункту можна дозволити вносити зміни будь-кому, хто встановлює з’єднання з вкзаних IP-адрес. Адреси має бути вказано у форматі списку відокремлених комами адрес або специфікацій мережі. Це корисно, якщо ви хочете, щоб сервер працював без розпізнавання, але не хочете втрачати можливості вносити зміни до ваших бібліотек calibre за допомогою calibredb. Зауважте, що вмикання цього параметра означає, що будь-хто, хто з'єднується із вказаних IP-адрес, зможе вносити зміни до ваших бібліотек calibre.

--url-prefix

Префікс для усіх адрес. Корисно, якщо ви хочете користуватися цим сервером за реверсивним проксі. Наприклад, можна скористатися префіксом адреси «/calibre».

--userdb

Шлях до бази даних користувачів, якою слід скористатися для розпізнавання. Дані у базі зберігаються у файлі SQLite. Для його створення скористайтеся --manage-users. Докладніше про керування користувачами: https://manual.calibre-ebook.com/uk/server.html#managing-user-accounts-from-the-command-line-only

--version

показати дані щодо версії програми і завершити роботу

--worker-count

Кількість потоків обробника під час обробки запитів.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibre-smtp

calibre-smtp

calibre-smtp [параметри] [відправник отримувач текст]

Надіслати поштове повідомлення за допомогою протоколу SMTP. У calibre-smtp передбачено два режими роботи. У режимі створення повідомлень ви вказуєте відправника, отримувача та текст повідомлення, і ці дані використовуються для надсилання повідомлення. У режимі фільтру calibre-smtp читає усе повідомлення зі стандартного джерела вхідних даних, STDIN, і надсилає його.

текст — визначає текст повідомлення електронної пошти. Якщо текст не буде вказано, вміст повідомлення електронної пошти буде прочитано зі STDIN. відправник — адреса електронної пошти відправника, а отримувач — адреса електронної пошти отримувача. Якщо повідомлення читається повністю із STDIN, під час узгодження надсилання із SMTP використовуються лише дані щодо відправника і отримувача, заголовки повідомлення не змінюються.

Якщо ви передаєте аргументи calibre-smtp, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--fork, -f

Створити відгалуження обробки і надіслати повідомлення у фоновому режимі. Якщо ви скористаєтеся цим параметром, вам слід також використати --outbox для обробки помилок під час надсилання повідомлення.

--help, -h

показати це довідкове повідомлення і завершити роботу

--localhost, -l

Назва локального вузла. Використовується для встановлення з’єднання із сервером SMTP.

--outbox, -o

Шлях до теки пошти (maildir) для зберігання повідомлень, які не вдасться надіслати.

--timeout, -t

Час очікування на встановлення з’єднання.

--verbose, -v

Режим з докладним виводом

--version

показати дані щодо версії програми і завершити роботу

COMPOSE MAIL

Параметри створення поштових повідомлень. Ігнорується, якщо не вказано текст.

--attachment, -a

Файл, який слід долучити до повідомлення електронної пошти

--subject, -s

Тема повідомлення електронної пошти

SMTP RELAY

Параметри, які слід використовувати для надсилання пошти за допомогою трансляційного сервера SMTP. calibre намагатиметься надсилати поштові повідомлення безпосередньо, якщо не вказано параметра –relay.

--cafile

Шлях до файла із об'єднаними сертифікатами CA у форматі PEM, яким слід скористатися для перевірки сертифіката сервера, якщо використовується TLS. Типово, використовуватимуться сертифікати CA системи.

--dont-verify-server-certificate

Не перевіряти сертифікат сервера, якщо з'єднання відбувається із використанням TLS. Це типова поведінка для версій calibre до версії 3.27. Якщо ви користуєтеся сервером-ретранслятором із самопідписаним або некоректним сертифікатом, ви можете скористатися цим пунктом для відновлення поведінки програми до версії 3.27.

--encryption-method, -e

Спосіб шифрування, який слід використати для з’єднання із трансляцією. Варіанти: TLS, SSL та NONE (немає). Типовим є TLS. ПОПЕРЕДЖЕННЯ: вибір варіанта NONE значно знижує захист вашої системи.

--password, -p

Пароль до трансляції

--port

Порт на сервері трансляції для з'єднання. Типовим є порт 465, якщо використовується шифрування SSL, і 25 за інших умов шифрування.

--relay, -r

Сервер SMTP трансляції для надсилання пошти.

--username, -u

Ім’я користувача трансляції

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- calibredb

calibredb

calibredb команда [параметри] [аргументи]

calibredb — інтерфейс командного рядка до бази даних calibre.
Передбачено декілька допоміжних команд, які документовано нижче.

calibredb можна скористатися для керування або базою даних calibre, вказаної шляхом, або Сервером даних calibre, запущеного або на локальному комп’ютері, або в інтернеті. Запустити Сервер даних calibre можна або за допомогою команди calibre-server, або з головного вікна програми calibre вибором пункту З’єднатись/Роздати  →
Запустити сервер даних. Оскільки calibredb може вносити зміни до ваших бібліотек calibre, вам слід спочатку налаштувати розпізнавання на сервері.
Це можна зробити у два способи:

- Якщо ви плануєте з’єднуватися лише з сервером, який запущено на тому самому комп’ютері, ви можете просто скористатися параметром --enable-local-write сервера даних, щоб уможливити для будь-якої програми, зокрема calibredb, запущеної на локальному комп’ютері, внесення змін до ваших даних calibre. Якщо ви запускаєте сервер із головного вікна програми calibre, цей пункт можна знайти у розділі Налаштування  →  Обмін мережею  →  Додатково.

- Якщо ви хочете увімкнути доступ інтернетом, вам слід налаштувати облікові записи користувачів на сервері і скористатися параметрами --username і --password програми calibredb для надання їй доступу. Ви можете налаштувати розпізнавання для calibre-server за допомогою параметра --enable-auth і скористатися параметром --manage-users для створення облікових записів користувачів. Якщо ви працюєте із сервером з головного вікна програми calibre, скористайтеся пунктом, Налаштування → Обмін мережею → Вимагати введення імені користувача і пароля.

Для встановлення з’єднання із запущеним сервером даних, передайте адресу сервера у параметрі --with-library. Див. документацію до цього параметра, щоб дізнатися більше і ознайомитися із прикладами.

- ЗАГАЛЬНІ ПАРАМЕТРИ

- list

- add

- ДОДАВАННЯ З ТЕК

- remove

- add_format

- remove_format

- show_metadata

- set_metadata

- export

- catalog

- ПАРАМЕТРИ EPUB

- saved_searches

- add_custom_column

- custom_columns

- remove_custom_column

- set_custom

- restore_database

- check_library

- list_categories

- backup_metadata

- clone

- embed_metadata

- search

- fts_index

- fts_search

ЗАГАЛЬНІ ПАРАМЕТРИ

--help, -h

показати це довідкове повідомлення і завершити роботу

--library-path, --with-library

Шлях до бібліотеки calibre. Типово, використовується шлях, який збережено у параметрах роботи програми. В також можете встановити з’єднання із сервером даних calibre для виконання дій із віддаленими бібліотеками. Для цього скористайтеся адресою у такому форматі: http://назва_вузла:порт/#ідентифікатор_бібліотеки, наприклад, http://localhost:8080/#mylibrary. Ідентифікатором бібліотеки є ідентифікатор бібліотеки на сервері даних. Ви можете скористатися спеціальним значенням параметра ідентифікатор_бібліотеки, «-», щоб отримати список ідентифікаторів бібліотек на сервері. Докладний опис налаштовування доступу за допомогою сервера даних можна знайти у розділі https://manual.calibre-ebook.com/uk/generated/uk/calibredb.html.

--password

Пароль для встановлення з’єднання із сервером даних calibre. Щоб пароль було прочитано із стандартного джерела введення, скористайтеся спеціальним значеннямв: <stdin>. Щоб пароль було прочитано з файла, скористайтеся такою конструкцією: <f:/path/to/file> (тобто <f: шлях до файла повністю і завершальний >). Символи «менше» і «більше» є обов'язковими. Не забудьте про їхнє екранування та лапки, якщо використовуєте їх у командній оболонці.

--timeout

Час очікування, у секундах, на встановлення з'єднання мережею із бібліотекою calibre. Типовим є час у дві хвилини.

--username

Ім’я користувача для встановлення з’єднання із сервером даних calibre

--version

показати дані щодо версії програми і завершити роботу

list

calibredb list [параметри]

Показати список книг у базі даних calibre.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--ascending

Впорядкувати дані за зростанням

--fields, -f

Поля, які слід показувати у списку книг бази даних. Поля у списку слід відокремлювати комами. Можливі поля: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Типовий список: title,authors. Особливим значенням «all» можна скористатися для вибору всіх полів. Крім вбудованих полів, назви яких наведено вище, можна скористатися нетиповими полями, доступ до яких здійснюється у форматі *назва_поля, наприклад, для нетипового поля #rating, слід скористатися назвою *rating

--for-machine

Вивести дані у форматі JSON, який є придатнішим для обробки комп’ютером. Призводить до ігнорування параметрів ширини рядків та роздільників.

--limit

Максимальна кількість показаних результатів. Типове значення: всі результати.

--line-width, -w

Максимальна ширина окремого рядка виведених даних. Типове значення визначається на основі розмірів екрана.

--prefix

Префікс всіх шляхів до файлів. Типовим є абсолютний шлях до теки бібліотеки.

--search, -s

Фільтрувати результати за запитом щодо пошуку. Настанови щодо форматування запитів для пошуку наведено у підручнику користувача. Типово не виконується жодного фільтрування.

--separator

Рядок, який відокремлюватиме поля. Типовим є пробіл.

--sort-by

Поза, за яким слід упорядковувати результати. Ви можете вказати декілька полів, відокремивши їхні записи комами. Доступні поля: author_sort, authors, comments, cover, formats, identifiers, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, size, tags, template, timestamp, title, uuid Типове значення: id

--template

Шаблон, який слід запустити, якщо у списку полів є «template». Типове значення: немає

--template_file, -t

Шлях до файла, що містить шаблон, який слід запустити, якщо у списку полів є «template». Типове значення: немає

--template_heading

Заголовок стовпчика шаблонів. Типове значення: template. Цей параметр буде проігноровано, якщо встановлено параметр --for-machine

add

calibredb add [параметри] файл1 файл2 файл3 ...

Додати вказані файли як книги до бази даних. Ви також можете вказати теки.
Докладніший опис параметрів, пов’язаних з теками, наведено нижче.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--authors, -a

Встановити авторів для доданих книг

--automerge, -m

Якщо буде виявлено книги із однаковими назвами і авторами, об'єднати вхідні формати (файли) автоматично із наявними записами книг. Значення «ignore» означає, що дублікати форматів буде відкинуто. Значення «overwrite» означає, що дублікати форматів у бібліотеці буде перезаписано новими доданими файлами. Значення «new_record» означає, що дублікати форматів буде записано до нового запису книги.

--cover, -c

Шлях до зображення обкладинки, яке слід використати для доданої книги

--duplicates, -d

Додати записи книг до бази даних, навіть якщо такі записи у цій базі вже створено. Порівняння виконуватиметься за назвами або авторами книг. Зауважте, що пріоритет буде надано параметру --automerge.

--empty, -e

Додати порожню книгу (книгу без формату)

--identifier, -I

Встановити ідентифікатори для цієї книги, наприклад -I asin:XXX -I isbn:YYY

--isbn, -i

Встановити ISBN для доданих книг

--languages, -l

Список мов, відокремлених комами (варто користуватися кодами мов ISO639, втім, передбачено розпізнавання і деяких повних назв мов)

--series, -s

Встановити цикл для доданих книг

--series-index, -S

Встановити номер у циклі для доданих книг

--tags, -T

Встановити мітки для доданих книг

--title, -t

Встановити заголовки для доданих книг

ДОДАВАННЯ З ТЕК

Параметри, які керують додаванням книг з тек. Типово, програма додаватиме лише файли книг, суфікси назв яких є відомими як суфікси назв файлів електронних книг.

--add

Взірець (загальний) назви файла. Файли, назви яких відповідатимуть взірцеві буде додано під час пошуку файлів у теках, навіть якщо суфікс назви не належить до відомого програмі набору суфіксів назв файлів електронних книг. Можна вказати декілька взірців.

--ignore

Взірець (загальний) назви файла. Файли, назви яких відповідатимуть взірцеві буде проігноровано під час пошуку файлів у теках. Можна вказати декілька взірців. Якщо, наприклад, вказати взірець *.pdf, буде проігноровано усі файли PDF.

--one-book-per-directory, -1

Припускати, що у кожній теці міститься лише одна книга, всі файли у цьому каталозі є цією книгою, записаною у різних форматах

--recurse, -r

Обробляти теки рекурсивно

remove

calibredb remove ідентифікатори

Вилучити книги, визначені ідентифікаторами з бази даних. Ідентифікатори у списку слід відокремлювати комами. Отримати номери ідентифікаторів можна за допомогою команди search. Приклад: 23,34,57-85 (у діапазон ідентифікаторів не включатиметься запис діапазону, у нашому прикладі 85).

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--permanent

Не використовувати Смітник

add_format

calibredb add_format [параметри] ідентифікатор файл_книги

Додати книгу з файла файл_книги до доступних форматів для логічної книги, визначеної ідентифікатором. Ідентифікатор може бути визначено за допомогою команди search. Якщо запис формату вже існує, його буде замінено, якщо не вказано параметр вимикання такої заміни.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--as-extra-data-file

Додати файл як додатковий файл даних, а не формат книги

--dont-replace

Не замінювати формат, якщо файл у цьому форматі вже існує

remove_format

calibredb remove_format [параметри] ідентифікатор формат

Вилучити формат з запису логічної книги, визначеної ідентифікатором. Визначити ідентифікатор можна за допомогою команди search. Формат має бути визначено суфіксом назви файла, наприклад LRF, TXT або EPUB. Якщо у записі логічної книги не міститиметься запису формату, ніяких дій не буде виконано.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

show_metadata

calibredb show_metadata [параметри] ідентифікатор

Показати метадані, що зберігаються у базі даних calibre для книги з ідентифікатором «ідентифікатор».
«ідентифікатор» є ідентифікатором, який повертає команда search.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--as-opf

Вивести метадані у форматі OPF (XML)

set_metadata

calibredb set_metadata [параметри] ідентифікатор_книги [/шлях/до/метадані.opf]

Встановити у базі метаданих calibre для книги, вказаної за ідентифікатором, метадані з файла OPF метадані.opf. ідентифікатором має бути значення зі списку, який надається командою search. Коротку довідку щодо формату OPF можна отримати за допомогою ключа –as-opf, доданого до команди show_metadata.
Крім того, можна встановлювати метадані для окремих полів за допомогою параметра –field. Якщо використовується параметр –field, вказувати файл OPF не потрібно.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--field, -f

Поле, значення якого слід встановити. Формат визначення поля такий: назва_поля:значення. Приклад: --field tags:tag1,tag2. Скористайтеся --list-fields, щоб отримати список всіх назв полів. Цей параметр можна використовувати декілька разів з метою встановлення значень для декількох полів. Зауваження: для полів мов слід використовувати коди мов ISO639 (наприклад en для англійської, uk для української тощо). Синтаксис ідентифікаторів: --field identifiers:isbn:XXXX,doi:YYYYY. Для булевих полів (полів «так/ ні») слід використовувати значення true та false або yes та no.

--list-fields, -l

Показати список назв полів метаданих, які може бути використано з параметром --field

export

calibredb export [параметри] ідентифікатори

Експортувати книги, вказані за допомогою ідентифікаторів (розділених у списку комами) до файлової системи.
Під час дії з експортування зберігаються всі формати книги, зображення обкладинки книги і метадані (у форматі файла OPF). Також буде збережено усі файли додаткових даних, які пов’язано із книгою.
Визначити номери ідентифікаторів можна за допомогою команди search.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--all

Експортувати всі книги у бібліотеці з ігноруванням списку ідентифікаторів.

--dont-asciiize

Наказати calibre перетворити усі символи у назвах файлів, яких немає у англійській абетці, на символи англійської абетки. Це корисно для зберігання даних у застарілих файлових системах без повної підтримки назв файлів у форматі Unicode. Визначення цього перемикача вимкне вказану поведінку.

--dont-save-cover

Зазвичай calibre зберігатиме зображення обкладинки у окремому файлі разом з файлами електронної книги. Визначення цього перемикача вимкне вказану поведінку.

--dont-save-extra-files

Зберегти усі файли даних, які пов'язано із книгою, при зберігання книги Визначення цього перемикача вимкне вказану поведінку.

--dont-update-metadata

Зазвичай calibre оновлює метадані у збережених файлах на основі даних бібліотеки calibre. Уповільнює запис на диск. Визначення цього перемикача вимкне вказану поведінку.

--dont-write-opf

Зазвичай calibre записуватиме метадані до окремого файла OPF разом з файлами електронної книги. Визначення цього перемикача вимкне вказану поведінку.

--formats

Відокремлений комами список форматів, у яких слід зберігати кожну книгу. Типово книга зберігатиметься у всіх форматах.

--progress

Сповіщати про поступ

--replace-whitespace

Замінити пробіли підкреслюваннями.

--single-dir

Експортувати всі книги до однієї теки

--template

Шаблон для керування назвами файлів та структурою тек збережених файлів. Типовим є шаблон «{author_sort}/{title}/{title} - {authors}», у разі використання якого книги буде збережено до окремих підтек за авторами, назви файлів міститимуть назви книг і імена авторів. Доступні можливості керування: {author_sort, authors, id, isbn, languages, last_modified, pubdate, publisher, rating, series, series_index, tags, timestamp, title}

--timefmt

Формат, у якому слід показувати дати. %d — день, %b — місяць, %m — номер місяця, %Y — рік. Типовим форматом є %b, %Y

--to-dir

Експортувати книги до вказаної теки. Типовою текою є .

--to-lowercase

Перетворити символи шляхів на нижній регістр.

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.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--ids, -i

Список значень ідентифікаторів бази даних, відокремлених комами, для каталогізації. Якщо визначено, --search буде проігноровано. Типове значення: всі значення (all)

--search, -s

Фільтрувати результати за запитом щодо пошуку. Настанови щодо форматування запитів для пошуку наведено у підручнику користувача. Типово не виконується жодного фільтрування

--verbose, -v

Показувати докладні виведені дані. Корисно для діагностики.

ПАРАМЕТРИ EPUB

--catalog-title

Заголовок створеного каталогу, використаний як заголовок у метаданих. Типове значення: «My Books» Застосування: формати виведення AZW3, EPUB, MOBI

--cross-reference-authors

Створювати перехресні посилання у розділі авторів для книг з декількома авторами. Типове значення: «False» Застосовується до таких форматів виведення даних: AZW3, EPUB, MOBI

--debug-pipeline

Зберігати виведені дані на різних кроків у вказаній теці. Корисно, якщо ви не впевнені, на якому кроці сталася помилка. Типове значення: «None» Застосування: формати виведення AZW3, EPUB, MOBI

--exclude-genre

Формальний вираз, що описує мітки для виключення жанрів. Типове значення: «[.+]|^+$» виключає мітки у квадратних дужках, наприклад «[Project Gutenberg]», та «+», типову мітку прочитаних книг. Застосування: формати виведення AZW3, EPUB, MOBI

--exclusion-rules

Визначає правила виключення книг зі створеного каталогу. Модель правила виключення записується у форматі ('<назва правила>','Tags','<список міток, відокремлених комами>') або ('<назва правила>','<нетиповий стовпчик>','<шаблон>'). Приклад: (('Архівовані книги','#status','Archived'),) виключить книги зі значенням «Archived» у нетиповому стовпчику «status». Якщо визначено декілька правил, буде застосовано всі з них. Типове значення: (('Архівовані книги','#status','Archived'),) виключить книги зі значенням «Archived» у нетиповому стовпчику «status». Якщо визначено декілька правил, буде застосовано всі з них. Типове значення: "(('Catalogs','Tags','Catalog'),)" Застосовується до форматів результатів AZW3, EPUB, MOBI

--generate-authors

Включення розділу «Автори» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--generate-descriptions

Включення розділу «Описи» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--generate-genres

Включення розділу «Жанри» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--generate-recently-added

Включення розділу «Нещодавно додані» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--generate-series

Включення розділу «Цикли» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--generate-titles

Включення розділу «Заголовки» до каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

--genre-source-field

Поле джерела даних для розділу жанру. Типове значення: «Теґи» Стосується таких форматів виведення даних: AZW3, EPUB, MOBI

--header-note-source-field

Нетипове поле, що містить текст нотатки для додавання до заголовка опису. Типове значення: «» Застосування: формати виведення AZW3, EPUB, MOBI

--merge-comments-rule

#<нетипове поле>:[before|after]:[True|False] визначає: <нетипове поле> — нетипове поле, що містить нотатки, які має бути додано до коментарів [before|after] — розташування нотаток відносно коментарів [True|False] — визначає чи слід додавати горизонтальну лінію між нотатками і коментарями Типове значення: «::» Застосування: формати виведення AZW3, EPUB, MOBI

--output-profile

Визначає профіль виведення даних. Іноді профіль виведення даних потрібен для оптимізації каталогу для пристрою. Наприклад, профілі «kindle» і «kindle_dx» створюють структурований зміст з розділами і статтями. Типове значення: «None» Формати: формати виведення AZW3, EPUB, MOBI

--prefix-rules

Визначає правила, використані для включення префіксів, що позначають прочитані книги, пункти списку бажаних книг та інші визначені користувачами префікси. Модель правила префіксів є такою: ('<назва правила>','<поле джерела>','<шаблон>','<префікс>'). Якщо буде визначено декілька правил, застосовуватиметься лише перше відповідне з них. Типове значення: "(('Read books','tags','+','✓'),('Wishlist item','tags','Wishlist','×'))" Застосовується до форматів результатів AZW3, EPUB, MOBI

--preset

Використовувати вказаний за назвою шаблон, створений за допомогою графічних інструментів збирання каталогу. У такому шаблоні визначено всі параметри збирання каталогу. Типове значення: 'None' Застосування: формати виведення результатів AZW3, EPUB, MOBI.

--thumb-width

Основний розмір (у дюймах) для зображень обкладинок у каталозі. Діапазон: 1.0 — 2.0 Типове значення: «1.0» Застосування: формати виведення AZW3, EPUB, MOBI

--use-existing-cover

Замінювати зображення обкладинок під час створення каталогу. Типове значення: «False» Застосування: формати виведення AZW3, EPUB, MOBI

saved_searches

calibredb збережені_пошуки [параметри] (list|add|remove)

Керування збереженими записами пошуків, що зберігаються у цій базі даних.
Якщо ви спробуєте додати запит з назвою, яку вже записано до бази даних, попередній запис буде замінено новим.

Синтаксис для додавання:

calibredb збережені_пошуки add назва_пошуку вираз_пошуку

Синтаксис для вилучення:

calibredb збережені_пошуки remove назва_пошуку

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

add_custom_column

calibredb add_custom_column [параметри] мітка назва тип_даних

Створити нетиповий стовпчик. «мітка» — комп’ютерна назва стовпчика. Мітка не повинна містити пробілів і двокрапок. «назва» — зручна для читання назва стовпчика. «тип_даних» має бути одним з таких значень: bool, comments, composite, datetime, enumeration, float, int, rating, series, text

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--display

Набір параметрів для налаштовування обробки даних у цьому стовпчику. Має бути рядком JSON. Для стовпчиків нумерації скористайтеся --display"{\ "enum_values\ ":[\ "значення1\ ", \ "значення2\ "]}" Передбачено багато варіантів передавання значень показаній змінній. Серед варіантів за типами стовпчиків: мішаний тип: composite_template, composite_sort, make_category,contains_html, use_decorations дата і час: date_format нумерація: enum_values, enum_colors, use_decorations ціле, десятковий дріб: number_format текст: is_names, use_decorations Найкращим способом визначення відповідних комбінацій є створення нетипового стовпчика відповідного типу за допомогою графічного інтерфейсу з наступним вивченням резервної копії OPF книги (переконайтеся, що новий OPF було створено після додавання стовпчика). У такому OPF можна знайти JSON для показу ("display") нового стовпчика.

--is-multiple

У цьому стовпчику зберігаються дані, подібні до міток (тобто значення, відокремлені комами). Застосовується, лише якщо типом даних є «text».

custom_columns

calibredb custom_columns [параметри]

Показати доступні нетипові стовпчики. Виводить мітки та ідентифікатори стовпчиків.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--details, -d

Показати параметри кожного стовпчика.

remove_custom_column

calibredb remove_custom_column [параметри] мітка

Вилучити нетиповий стовпчик, визначений міткою. Переглянути список доступних стовпчиків можна за допомогою команди custom_columns.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--force, -f

Не питати про підтвердження

set_custom

calibredb set_custom [параметри] стовпчик ідентифікатор значення

Встановити значення у нетиповому стовпчику для книги, визначеної ідентифікатором.
Список ідентифікаторів можна отримати за допомогою команди search.
Отримати список назв нетипових стовпчиків можна отримати за допомогою

команди custom_columns.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--append, -a

Якщо у стовпчику зберігається декілька значень, дописати вказані значення до вже записаних замість заміни цих значень.

restore_database

calibredb restore_database [параметри]

Відновити цю базу даних на основі метаданих, що зберігаються у файлах OPF кожної з тек бібліотеки calibre. Корисно, якщо ваш файл metadata.db пошкоджено.

ПОПЕРЕДЖЕННЯ: у результаті виконання цієї команди вашу базу даних буде створено з нуля. Ви втратите всі збережені дані пошуку, категорії користувача, засоби обробки, збережені параметри перетворення окремих книг та нетипові рецепти.
Відновлені метадані будуть настільки точними, наскільки точними є дані, збережені у файлах OPF.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--really-do-it, -r

Виконати відновлення. Команду не буде виконано, якщо не буде вказано цього параметра.

check_library

calibredb бібліотека_для_перевірки [параметри]

Виконати деякі перевірки у файловій системі бібліотеки. Звіти буде invalid_titles, extra_titles, invalid_authors, extra_authors, missing_formats, extra_formats, extra_files, missing_covers, extra_covers, failed_folders

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--csv, -c

Виведення у форматі CSV

--ignore_extensions, -e

Список суфіксів назв, які слід ігнорувати, відокремлений комами. Типове значення: всі (all)

--ignore_names, -n

Список назв, які слід ігнорувати, відокремлений комами. Типове значення: всі (all)

--report, -r

Список звітів, відокремлених комами. Типове значення: всі (all)

--vacuum-fts-db

Очистити базу даних повнотекстового пошуку. Процедура може бути дуже повільною і вимогливою до пам'яті, залежно від розміру бази даних.

list_categories

calibredb list_categories [параметри]

Створити звіт щодо даних категорій у базі даних. Отримані дані еквівалентні до даних, показаних на панелі навігації мітками.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--categories, -r

Список назв фільтрів категорій, відокремлених комами. Типове значення: всі (all)

--csv, -c

Виведення у форматі CSV

--dialect

Тип файл CSV, який слід створити. Варіанти: excel, excel-tab, unix

--item_count, -i

Виводити лише кількість пунктів у категорії замість кількостей елементів у пункті категорії

--width, -w

Максимальна ширина окремого рядка виведених даних. Типове значення визначається на основі розмірів екрана.

backup_metadata

calibredb backup_metadata [параметри]

Створити резервну копію метаданих, що зберігаються у базі даних у окремих файла OPF у кожній з тек книг. Зазвичай, така дія виконується автоматично, але ви можете наказати програмі виконати примусове повторне створення файлів OPF, якщо вкажете параметр –all.

Зауважте, що зазвичай потреби у цій команді немає, оскільки резервні копії файлів OPF створюються автоматично у разі внесення змін до метаданих.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--all

Зазвичай, ця програма обробляє лише ті книги, файли OPF яких застаріли. За допомогою цього параметра ви можете наказати програмі обробляти всі файли книг.

clone

calibredb clone шлях/до/нової/бібліотеки

Створити клон поточної бібліотеки. За допомогою цієї програми можна створити нову порожню бібліотеку з тими самими нетиповими стовпчиками, віртуальними бібліотеками та іншими параметрами, що і у поточній бібліотеці.

У клонованій бібліотеці не буде жодної книги. Якщо вам потрібно створити дублікати бібліотеки з усіма книгами, просто скористайтеся інструментами для керування файлами і скопіюйте теку бібліотеки.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

embed_metadata

calibredb embed_metadata [параметри] ідентифікатор_книги

Оновити метадані файлів книги, що зберігаються у бібліотеці calibre на основі метаданих з бази даних calibre. Зазвичай, метадані оновлюються лише під час експортування файлів з calibre. Ця команда корисна, якщо ви хочете оновити файли на місці. Зауважте, що у файлах різних форматів передбачено зберігання різних наборів метаданих. Щоб оновити метадані у всіх книгах, скористайтеся спеціальним значенням ідентифікатора — «all». Крім того, можна вказати декілька ідентифікаторів книг, відокремивши їх пробілами, або діапазони ідентифікаторів з межами, відокремленими дефісами. Приклад: calibredb embed_metadata 1 2 10-15 23

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--only-formats, -f

Оновити метадані лише у файлах вказаного формату. Можна вказувати декілька таких параметрів, якщо потрібно обробити файли декількох форматів. Якщо параметр не вказано, програма оновлює усі формати.

search

calibredb search [параметри] вираз для пошуку

Шукати у бібліотеці записи із вказаним ключем для пошуку. Повертає список ідентифікаторів відповідних книг, відокремлених комами. Формат виведення даних корисний для передавання іншим командам, які приймають вхідні дані у форматі списку ідентифікаторів.

Виразом для пошуку може бути будь-який запис потужною мовою запитів calibre. Приклад: calibredb search author:asimov „title:»i robot»“

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--limit, -l

Максимальна кількість повернутих результатів. Типово буде повернуто усі результати.

fts_index

calibredb fts_index [параметри] enable/disable/status/reindex

Керування процедурою індексування для повнотекстового пошуку.

enable — увімкнути індексування для повнотекстового пошуку для цієїї бібліотеки disable — вимкнути індексування для повнотекстового пошуку для цієї бібліотеки status — показати поточний стан індексування reindex — можна скористатися для повторного індексування певних книг або

усієї бібліотеки. Щоб повторно індексувати якісь книги, вкажіть ідентифікатори книг як додаткові аргументи після команди reindex. Якщо ідентифікаторів книг не буде вказано, буде виконано повторне індексування усієї бібліотеки.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--indexing-speed

Швидкість індексування. Скористайтеся швидким індексуванням з використанням усіх ресурсів комп'ютера або повільним, якщо слід заощаджувати ресурси на індексуванні. Зауважте, що програма скидатиме швидкість на повільну після кожного виклику.

--wait-for-completion

Очікувати на завершення індексування усіх книг, періодично показуючи дані щодо поступу індексуування

fts_search

calibredb fts_search [параметри] вираз для пошуку

Виконати повнотекстовий пошук в усій бібліотеці або її підмножині.

Якщо ви передаєте аргументи calibredb, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

--do-not-match-on-related-words

Встановлювати відповідність лише за точним словом, а не за пов'язаними з ним словами. Таким чином, пошук «виправлення» не призводитиме до показу відповідника «виправити».

--include-snippets

Включати фрагменти тексту навколо кожного відповідника. Зауважте, що це значно уповільнить пошук.

--indexing-threshold

Частка бібліотеки у відсотках, яку має бути індексовано, перш ніж повнотекстовий пошук буде увімкнено. Типовим є значення 90.

--match-end-marker

Позначка, яку буде використано для кінця відповідного слова у фрагменті

--match-start-marker

Позначка, яку буде використано для початку відповідного слова у фрагменті

--output-format

Формат виведення результатів пошуку. Може мати значення «text» для звичайного тексту або «json» для виведення у форматі JSON.

--restrict-to

Обмежити перелік книг для пошуку за допомогою якогось виразу для пошуку або списку ідентифікаторів. Приклад: ids:1,2,3 — обмежити пошук вказаними ідентифікаторами, або search:tag:щось — обмежити пошук книгами із міткою «щось».

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- ebook-convert

ebook-convert

ebook-convert вхідний_файл вихідний_файл [параметри]

Виконати перетворення електронної книги з одного формату до іншого.

вхідний_файл є назвою початкового файла, а вихідний_файл — файла результатів . Обидва слід вказати як перші два аргументи команди.

Формат перетворення електронної книги визначається за суфіксом назви файла вихідний_файл. вихідний_файл також можна вказати у особливому форматі — .EXT, де EXT є його суфіксом назви. У цьому випадку назви вхідного і вихідного файлів будуть збігатися. Врахуйте, що назви файлів не можуть починатися з дефіса. В результаті, якщо вихідний_файл не має суфікса назви, він визначається як тека, — до цієї теки буде записано дані у форматі «open ebook» (OEB), які є збіркою з файлів HTML. Ці файли, як правило, надсилаються до модуля виведення даних.

Після визначення вхідного та вихідного файлів ви можете налаштувати перетворення, вказавши різні параметри. Доступні параметри залежать від формату вхідного і вихідного файлів. Для отримання довідки вкажіть вхідний і вихідний файл, потім скористайтеся параметром -h.

Для отримання повної інформації щодо системи перетворення ознайомтеся з Перетворення даних електронних книг

Якщо ви передаєте аргументи ebook-convert, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

Параметри і типові значення параметрів можуть бути різними, залежно від формату вхідних і вихідних даних. Отже, завжди варто спочатку з ними ознайомитися за допомогою такої команди:

ebook-convert myfile.input_format myfile.output_format -h

Нижче наведено параметри, які є спільними для усіх перетворень, а далі, параметри, які є специфічними для кожного з форматів вхідних та вихідних даних.

- ВХІДНІ ПАРАМЕТРИ

- ВИХІДНІ ПАРАМЕТРИ

- ВИГЛЯД І ПОВЕДІНКА

- ЕВРИСТИЧНА ОБРОБКА

- ПОШУК З ЗАМІНОЮ

- ВИЯВЛЕННЯ СТРУКТУРИ

- ЗМІСТ

- МЕТАДАНІ

- ДІАГНОСТИКА

--help, -h

показати це довідкове повідомлення і завершити роботу

--input-profile

Вкажіть профіль введення. Профіль введення вказує засобу перетворення, як інтерпретувати різну інформацію всередині вхідного документа. Наприклад, роздільна здатність залежить від довжини (довжини в пікселях). Варіанти: cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, irexdr1000, irexdr800, kindle, msreader, mobipocket, nook, sony, sony300, sony900

--list-recipes

Показати список назв вбудованих рецептів. Ви можете створити електронну книгу на основі вбудованого рецепту ось так: ebook-convert "Назва рецепту.recipe" файл_результату.epub

--output-profile

Вкажіть вихідний профіль. Вихідний профіль вказує засобу перетворення, як оптимізувати створений документ для вказаного пристрою, втім, потреба у такій оптимізації виникає нечасто. Варіанти:cybookg3, cybook_opus, default, hanlinv3, hanlinv5, illiad, ipad, ipad3, irexdr1000, irexdr800, jetbook5, kindle, kindle_dx, kindle_fire, kindle_oasis, kindle_pw, kindle_pw3, kindle_scribe, kindle_voyage, kobo, msreader, mobipocket, nook, nook_color, nook_hd_plus, pocketbook_inkpad3, pocketbook_lux, pocketbook_hd, pocketbook_900, pocketbook_pro_912, galaxy, sony, sony300, sony900, sony-landscape, sonyt3, tablet, generic_eink, generic_eink_large, generic_eink_hd

--version

показати дані щодо версії програми і завершити роботу

ВХІДНІ ПАРАМЕТРИ

Параметри для керування опрацюванням вхідного файла mobi

--input-encoding

Вкажіть кодування знаків для вхідного документа. Якщо кодування вказано тут, то це перезапише всі кодування, які мав документ в собі. Особливо придатно для документів, які не мають вказаного кодування, або ж мають вказане неправильне кодування.

ВИХІДНІ ПАРАМЕТРИ

Параметри для керування опрацюванням вихідного файла epub

--dont-split-on-page-breaks

Вимкнути розрізання на межах сторінок. Зазвичай, файли вхідних даних автоматично поділяються на два файли у місці кожної межі сторінки. Таким чином можна отримувати електронні книги, які швидше обробляються і потребують для читання менше ресурсів. Розрізання є доволі повільною процедурою, тому якщо у вашому файлі дуже багато меж сторінок, вам слід вимкнути розрізання на межі сторінок.

--epub-flatten

Цей пункт може знадобитися, лише якщо ви маєте намір використовувати EPUB для читання у FBReaderJ. Програма спростить файлову систему у EPUB, розташувавши всі файли на верхньому рівні.

--epub-inline-toc

Вставити вбудований зміст, який буде показано як частину основного вмісту книги.

--epub-max-image-size

Максимальний розмір зображення (ширина x висота). Значення none означає «використовувати розмір екрана з профілю виведення даних». Значення profile означає, що максимальний розмір не встановлено. Наприклад, значення 100x200 призведе до того, що буде змінено розмір усіх зображень так, щоб їхня ширина не перевищувала 100 пікселів, а їхня висота не перевищувала 200 пікселів. Зауважте, що це стосується лише розміру самих файлів зображень. Будь-яке задане зображення може бути показано з використанням іншого розміру, залежно від стилю, який застосовано до нього у документі.

--epub-toc-at-end

Розташувати вбудований зміст наприкінці книги, а не на її початку.

--epub-version

Версія EPUB файла, який слід створити. EPUB 2 є найрозсповсюдженішим і найсуміснішим. Використовуйте версію EPUB 3, лише якщо справді цього потребуєте.

--extract-to

Видобути вміст створеного файла EPUB до вказаної теки. Перед видобуванням вміст теки буде вилучено, тому вам слід бути обережними.

--flow-size

Розділяти на частини усі файли HTML більшого розміру, аніж вказано тут (у кБ). Це необхідно, оскільки більшість пристроїв читання EPUB не можуть працювати з файлами великих розмірів. Типовий розмір у 260 кБ — це розмір, сумісний з Adobe Digital Editions. Встановіть значення 0, щоб вимкнути поділ на основі розміру.

--no-default-epub-cover

Зазвичай, якщо у файлі вхідних даних не міститься зображення обкладинки, а ви не вказали цього зображення явно, буде створено стандартну обкладинка з назвою, іменем автора тощо. За допомогою цього пункту можна вимкнути створення таких обкладинок.

--no-svg-cover

Не використовувати зображення SVG для обкладинок книжок. Позначте цей пункт, якщо створений файл EPUB буде використано на пристрої, на якому не передбачено підтримки SVG, зокрема на iPhone або JetBook Lite. Якщо цей пункт не буде позначено, на таких пристроях обкладинку буде показано як порожню сторінку.

--preserve-cover-aspect-ratio

Якщо буде позначено цей пункт, у разі використання зображення обкладинки у форматі SVG масштаб буде підібрано так, щоб зображення займало максимальну площу екрана, але без зміни співвідношення його розмірів (відношення ширини до висоти). Це може призвести до появи білих смуг з боків або згори і знизу, але зображення не буде викривлено. Якщо пункт не буде позначено, можливі невеликі викривлення, але білих смуг навколо зображення не буде.

--pretty-print

Якщо вказано, додаток виведення даних спробує створити зручні для читання виведені дані. Може не мати жодного ефекту для деяких додатків виведення даних.

--toc-title

Заголовок для будь-якого згенерованого in-line змісту.

ВИГЛЯД І ПОВЕДІНКА

Параметри для керування за зовнішнім виглядом вихідного файла

--asciiize

Перетворити символи Unicode на символи ASCII. Будьте обережні з використанням цієї можливості, оскільки всі символи Unicode буде замінено на символи ASCII. Наприклад, «Pelé» буде замінено на «Pele». Крім того, зауважте, що якщо у певного символу є декілька представлень (наприклад, спільні символи китайської та японської мов), представлення буде виконано відповідно до поточної мови інтерфейсу calibre.

--base-font-size

Розмір основного шрифту у пунктах. На основі цього розміру буде визначено масштаб всіх інших шрифтів у книзі. Якщо буде вибрано більший розмір, всі шрифти у результаті будуть більшими. Типово, коли значенням є нуль, розмір основного шрифту буде вибрано на основі вибраного вами профілю виведення даних.

--change-justification

Змінити текст вирівнювання. У разі визначення значення «Ліворуч» перетворює всі вирівняні фрагменти тексту на вирівняні ліворуч (тобто невирівняні) фрагменти. У разі визначення значення «Вирівняти» перетворює всі невирівняні фрагменти на вирівняні. У разі визначення значення «Оригінал» (типове) початкове вирівнювання тексту не буде змінено. Зауважте, що можливість вирівнювання передбачено лише у деяких з форматів виведення даних.

--disable-font-rescaling

Заборонити масштабування шрифтів.

--embed-all-fonts

Вбудувати усі шрифти, які використано у документі і ще не вбудовано до нього. Програма виконає пошук шрифтів на вашому комп’ютері і, якщо шрифти буде знайдено, вбудує їх до документа, зокрема у форматі EPUB, AZW3, DOCX та PDF. Будь ласка, зважте на умови ліцензування, пов’язані з вбудовуванням шрифтів до цього документа.

--embed-font-family

Вбудувати до електронної книги вказану гарнітуру шрифтів. Таким чином можна визначити «основний» шрифт, який буде використано у книзі. Якщо у документі з вхідними даними визначаються власні шрифти, вони матимуть вищий за основний шрифт пріоритет. Для вилучення шрифтів з вхідних даних ви можете скористатися пунктом фільтрування даних щодо стилю. Зауважте, що вбудовування шрифтів працює лише у деяких форматах виведення даних, зокрема EPUB, AZW3 та DOCX.

--expand-css

Типово, calibre використовуватиме скорочену форму різноманітних властивостей CSS, зокрема полів, фаски, рамки тощо. За допомогою цього параметра можна наказати програмі використовувати розширену форму. Зауважте, що записи CSS завжди розгортаються під час створення файлів EPUB, якщо встановлено один з профілів виведення даних Nook, оскільки у таких профілях не передбачено обробки скорочених записів CSS.

--extra-css

Шлях до таблиці стилів CSS або код CSS. Вказаний код CSS буде додано до правил щодо стилів з файла початкових даних, отже ним можна скористатися для змінити початкових правил.

--filter-css

Список властивостей CSS, розділених комами, записи з якого слід вилучити з усіх правил стилів CSS. Корисно, якщо визначення даних стилів запобігає використанню параметрів вашого пристрою. Приклад: font-family,color,margin-left,margin-right

--font-size-mapping

Список відповідності назв розмірів шрифтів у CSS розмірам шрифтів у пунктах. Приклад: 12,12,14,16,18,20,22,24. У прикладі вказано відповідність всіх розмірів від xx-малі до xx-великі, останній розмір відповідає величезному шрифту (huge). У алгоритмі масштабування шрифтів ці значення використовуються для зміни масштабів шрифтів. Типово використовується список відповідності на основі вибраного вами профілю обробки даних.

--insert-blank-line

Вставте порожній рядок між абзацами. Не буде працювати, якщо вихідний файл не використовує абзаци (<p> або <div> теґи).

--insert-blank-line-size

Встановити висоту вставлених порожніх рядків (у em). Висота рядків між абзацами буде вдвічі більшою за вказане тут значення.

--keep-ligatures

Зберігати лігатури у поточному вхідному документі. Лігатура — це особливий спосіб розташовування пар символів, зокрема ff, fi, fl тощо. На більшості пристроїв для читання не передбачено підтримки лігатур у разі використання типових шрифтів, отже лігатури навряд чи буде показано відповідним чином. Типово, calibre перетворить лігатури на пари звичайних символів. Якщо буде позначено цей пункт, програма зберігатиме лігатури.

--line-height

Висота рядка у пунктах. Визначає інтервал між послідовними рядками тексту. Використовується лише для елементів, для яких не визначено власної висоти рядка. Здебільшого, краще користуватися мінімальною висотою рядка. Типово, програма не вноситиме змін до висоти рядка.

--linearize-tables

У деяких документах з вадами у форматуванні таблиці використовуються для керування компонуванням тексту на сторінці. Під час перетворення таких документів часто текст виповзає за сторінку або виникають інші помітні помилки у форматуванні. За допомогою цього пункту можна наказати програмі видобути вміст з таблиць і показати його у звичайному послідовному представленні.

--margin-bottom

Вкажіть ширину нижнього поля сторінки у пунктах. Типове значення — 5.0. Визначення від’ємного значення призведе до того, що поля просто не буде (поля, визначені у початковому документі, буде збережено). Примітка: у форматах із жорстким розбиттям на сторінки, зокрема PDF і DOCX, використовуються власні параметри полів, які мають вищий пріоритет за ці.

--margin-left

Вкажіть ширину лівого поля сторінки у пунктах. Типове значення — 5.0. Визначення від’ємного значення призведе до того, що поля просто не буде (поля, визначені у початковому документі, буде збережено). Примітка: у форматах із жорстким розбиттям на сторінки, зокрема PDF і DOCX, використовуються власні параметри полів, які мають вищий пріоритет за ці.

--margin-right

Вкажіть ширину правого поля сторінки у пунктах. Типове значення — 5.0. Визначення від’ємного значення призведе до того, що поля просто не буде (поля, визначені у початковому документі, буде збережено). Примітка: у форматах із жорстким розбиттям на сторінки, зокрема PDF і DOCX, використовуються власні параметри полів, які мають вищий пріоритет за ці.

--margin-top

Вкажіть ширину верхнього поля сторінки у пунктах. Типове значення — 5.0. Визначення від’ємного значення призведе до того, що поля просто не буде (поля, визначені у початковому документі, буде збережено). Примітка: у форматах із жорстким розбиттям на сторінки, зокрема PDF і DOCX, використовуються власні параметри полів, які мають вищий пріоритет за ці.

--minimum-line-height

Мінімальна висота рядка у відсотках від обчисленого розміру шрифту елемента. calibre визначити висоту рядків так, щоб вона була більшою за це значення, незалежно від параметрів, визначених у вхідному документі. Встановлення нульового значення вимикає використання мінімальної висоти. Типовим значенням є 120%. Цьому пункту слід надавати перевагу перед безпосереднім визначенням висоти рядка. Ви, наприклад, можете встановити подвійний інтервал між рядками, якщо встановите тут значення 240.

--remove-paragraph-spacing

Вилучати інтервали між абзацами. Також встановити відступ абзацу у значення 1,5 em. Вилучення інтервалів не працюватиме, якщо у файлі початкових даних не використовуються абзаци (теґи <p> і <div>).

--remove-paragraph-spacing-indent-size

Під час вилучення calibre порожніх рядків між абзацами для полегшення читання автоматично встановлюється відступ абзацу. За допомогою цього пункту можна змінити ширину цього відступу (у одиницях em). Якщо встановити це значення від'ємним, використовуватиметься відступ, вказаний у початковому документі, тобто calibre його не змінюватиме.

--smarten-punctuation

Перетворити звичайні лапки, дефіси та багатокрапки на належні типографські відповідники. Докладніше про перетворення можна дізнатися зі сторінки https://daringfireball.net/projects/smartypants.

--subset-embedded-fonts

Обмежувати кількість символів у всіх вбудованих шрифтах. Множину символів шрифту буде обмежено до тієї, яка використовується у документі. Таким чином, файли шрифтів ставатимуть меншими. Корисно, якщо виконується вбудовування шрифту зі значним набором символів, з яких використовується лише незначна частина.

--transform-css-rules

Шлях до файла, що містить правила перетворення стилів CSS у цій книзі. Найпростішим способом створення такого файла є використання майстра для створення правил із графічного інтерфейсу calibre. Пункт цього майстра у діалоговому вікні перетворення: «Вигляд і поведінка -> Перетворити стилі». Після створення правил, ви можете скористатися кнопкою «Експортувати» для збереження правил до файла.

--transform-html-rules

Шлях до файла, що містить правила перетворення HTML у цій книзі. Найпростішим способом створення такого файла є використання майстра для створення правил із графічного інтерфейсу calibre. Пункт цього майстра у діалоговому вікні перетворення: «Вигляд і поведінка -> Перетворити HTML». Після створення правил, ви можете скористатися кнопкою «Експортувати» для збереження правил до файла.

--unsmarten-punctuation

Перетворити форматовані лапки, тире та трикрапки на звичайні еквіваленти.

ЕВРИСТИЧНА ОБРОБКА

Внести зміни до тексту або структури документа за допомогою типових взірців. Типово вимкнено. Скористайтеся –enable-heuristics, щоб увімкнути. Окремі дії можна буде вимкнути за допомогою параметрів –disable-*.

--disable-dehyphenate

Виконати аналіз слів з дефісами у документі. Для визначення дії, яку слід виконати з дефісом (зберегти чи вилучити) як словник використовуватиметься сам текст документа.

--disable-delete-blank-paragraphs

Вилучити з документа порожні абзаци, якщо їх розташовано між будь-якими іншими абзацами

--disable-fix-indents

Перетворити відступи, створені за допомогою декількох нероздільних об’єктів пробілів, у відступи, відповідні таблиці стилів CSS.

--disable-format-scene-breaks

Вирівняні ліворуч роздільники сцен буде вирівняно за центром. Гнучкі розриви сцен декількома порожніми рядками буде замінено горизонтальними лініями.

--disable-italicize-common-cases

Знайти типові слова та взірці, які позначають курсив, і відтворити курсив у тексті.

--disable-markup-chapter-headings

Виявити неформатовані заголовки і підзаголовки глав. Ці заголовки буде замінено теґами h2 і h3. Позначення цього пункту не призведе до створення змісту, але ним можна скористатися у поєднанні з пунктом виявлення структури для створення змісту.

--disable-renumber-headings

Шукати послідовності з теґів <h1> або <h2>. Такі теґи має бути перенумеровано, щоб запобігти поділу тексту посередині заголовків глави.

--disable-unwrap-lines

Скасувати перенесення рядків на основі пунктуації та іншого форматування.

--enable-heuristics

Увімкнути евристичну обробку. Цей пункт має бути позначено, щоб програма могла виконувати будь-яку евристичну обробку.

--html-unwrap-factor

Коефіцієнт, який буде використано для визначення довжини рядків, які слід з’єднувати. Коректні значення лежать у діапазоні від 0 до 1. Типовим є значення 0,4, трошки менше за половину довжини рядка. Якщо з’єднанню підлягають лише декілька рядків у документі, вам слід вибрати менше значення.

--replace-scene-breaks

Замінити роздільник сцен вказаним текстом. Типово буде використано текст з вхідного документа.

ПОШУК З ЗАМІНОЮ

Внести зміни до тексту або структури документа за допомогою визначених користувачем взірців.

--search-replace

Шлях до файла, який містить формальні вирази для пошуку із заміною. У файлі мають міститися один за одним рядки, у одному з яких має бути вказано формальний вираз, а у іншому — шаблон заміни (може бути порожнім). У формальному виразі має бути використано синтаксичні правила побудови формальних виразів Python. Слід також використовувати у файлі кодування UTF-8.

--sr1-replace

Замінник тексту, знайденого за допомогою sr1-пошуку.

--sr1-search

Шаблон пошуку (формальний вираз), який буде замінено під час sr1-пошуку.

--sr2-replace

Замінник тексту, знайденого за допомогою sr2-пошуку.

--sr2-search

Шаблон пошуку (формальний вираз), який буде замінено під час sr2-пошуку.

--sr3-replace

Замінник тексту, знайденого за допомогою sr3-пошуку.

--sr3-search

Шаблон пошуку (формальний вираз), який буде замінено під час sr3-пошуку.

ВИЯВЛЕННЯ СТРУКТУРИ

Контроль автоматичного визначення структури документа.

--chapter

Вираз XPath для виявлення заголовків глав. Типово теґи <h1> або <h2>, які містять слова «chapter», «book», «section», «prologue», «epilogue» або «part», а також теґи, що містять class="chapter", вважатимуться теґами заголовків глав. Вказаний вираз має оброблятися як список елементів. Щоб вимкнути виявлення глав, скористайтеся виразом «/». Докладніший опис цієї можливості можна знайти у настановах щодо XPath у підручнику користувача calibre.

--chapter-mark

Вкажіть спосіб позначення виявлених глав. Значення "pagebreak" призведе до вставлення розривів сторінок перед главами. Значення "rule" призведе до вставлення лінії перед главами. Значення "none" призведе до вимикання позначення глав, а значення "both" — до використання розривів сторінок і ліній для позначення глав.

--disable-remove-fake-margins

У деяких документах поля сторінки визначаються за допомогою лівого і правого поля для кожного з абзаців. calibre намагатиметься виявити і вилучити такі поля. Іноді може призвести до вилучення полів, які не слід вилучати. У такому разі ви можете вимкнути вилучення.

--insert-metadata

Вставити метадані книги на початку книги. Корисне, якщо на вашому пристрої для читання електронних книжок не передбачено можливості безпосереднього показу метаданих або пошуку за метаданими.

--page-breaks-before

Вираз XPath. Розриви сторінок буде вставлено до вказаних елементів. Щоб вимкнути, вкажіть такий вираз: /

--prefer-metadata-cover

Використати обкладинку з початкового файла, а не вказану обкладинку.

--remove-first-image

Вилучати перше зображення з початкової електронної книги Корисно, якщо у початковому документі першим зображенням є зображення обкладинки, яке не позначено відповідним чином. Збереження такого зображення призведе до появи у перетвореній книзі двох зображень обкладинок: початкового і створеного calibre.

--start-reading-at

Вираз XPath для визначення місця у документі, з якого слід розпочати читання. У деяких програмах для читання (найвідомішою з яких є програма для читання Kindle) це місце використовується як місце, з якого слід відкривати книгу. З докладнішими довідковими даними щодо користування цією можливістю можна ознайомитися у настановах щодо XPath у «Підручнику користувача» calibre.

ЗМІСТ

Керування автоматичним створенням змісту. Типово, якщо файл має зміст, його буде використовуватися замість автоматично створеного.

--duplicate-links-in-toc

Під час створення змісту на основі посилань у початковому документі дозволити дублювання записів, тобто дозволити використання записів з однаковим текстом, якщо ці записи посилаються на різні місця у тексті.

--level1-toc

Вираз XPath, який визначає всі мітки, які слід додати до змісту на першому рівні. Вказаний вираз має пріоритет над усіма формами автоматичного визначення. Приклади можна знайти у настановах з XPath у підручнику для користувачів calibre.

--level2-toc

Вираз XPath, який визначає всі мітки, які слід додати до змісту на другому рівні. Всі записи буде додано після попереднього запису першого рівня. Приклади можна знайти у настановах з XPath у підручнику для користувачів calibre.

--level3-toc

Вираз XPath, який визначає всі мітки, які слід додати до змісту на третьому рівні. Всі записи буде додано після попереднього запису другого рівня. Приклади можна знайти у настановах з XPath у підручнику для користувачів calibre.

--max-toc-links

Максимальна кількість посилань, які буде додано до змісту. 0 вимикає додавання. Типове значення: 50. Посилання буде додано до змісту, лише якщо буде виявлено меншу за вказану кількість глав.

--no-chapters-in-toc

Не додавати автоматично визначені розділи до змісту.

--toc-filter

Вилучити зі змісту записи, які відповідають вказаному формальному виразу. Відповідні записи і всі дочірні записи цих записів буде вилучено.

--toc-threshold

Якщо буде знайдено менше цієї кількості розділів, то посилання будуть встановлені на сторінку змісту. Типове значення: 6

--use-auto-toc

Зазвичай, якщо файл джерела вже містить дані змісту, його пріоритет буде вищим за пріоритет автоматично створеного змісту. Якщо буде використано цей пункт, завжди використовуватиметься автоматично створений зміст.

МЕТАДАНІ

Параметри для встановлення метаданих вихідного файла

--author-sort

Рядок, що використовується для сортування за автором.

--authors

Вказати авторів. Декілька авторів повинні бути розділені амперсандами (&).

--book-producer

Вказати того, ким було створено книгу.

--comments

Встановити опис електронної книги.

--cover

Встановити обкладинку для вказаного файла або URL

--isbn

Вказати ISBN книги.

--language

Встановити мову.

--pubdate

Встановити дату публікації (припускається визначеною у локальному часовому поясі, окрім випадків, коли часовий пояс вказано явно)

--publisher

Встановити запис видавця електронної книги.

--rating

Вказати оцінку. Може бути число між 1 і 5.

--read-metadata-from-opf, --from-opf, -m

Прочитати метадані з вказаного файла OPF. Прочитані з цього файла метадані перевизначать будь-які інші метадані у початковому файлі.

--series

Вказати цикл, до якого належить книжка.

--series-index

Вказати номер книги в циклі.

--tags

Вказати мітки для книги. Повинні бути розділені комами.

--timestamp

Встановити часову позначку для книги (більше ніде не використовується)

--title

Вказати заголовок

--title-sort

Версія заголовка, що використовується для сортування.

ДІАГНОСТИКА

Параметри для полегшення зневаджування перетворення

--debug-pipeline, -d

Зберігати виведені дані на різних кроках перетворення до вказаної теки. Корисно, якщо ви не впевнені, на якому кроці сталася помилка.

--verbose, -v

Рівень докладності. Вкажіть декілька разів, якщо рівень слід збільшити. Двом параметрам відповідає максимальний рівень докладності, одному — середній рівень. Якщо параметр не вказано, рівень докладності буде найнижчим.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- ebook-edit

ebook-edit

ebook-edit [параметри] [шлях_до_книги] [назва_файла_у_книзі...]

Запустити засіб редагування книг calibre. Додатково можна вказати назви файлів у книзі, які слід автоматично відкрити для редагування.

Якщо ви передаєте аргументи ebook-edit, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--detach

Від’єднати від контрольного термінала, якщо такий був (лише Linux)

--help, -h

показати це довідкове повідомлення і завершити роботу

--select-text

Текст, який слід позначити у книзі, коли її буде відкрито для редагування

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- ebook-meta

ebook-meta

ebook-meta файл_книги [параметри]

Прочитати або записати метадані до файлі електронних книг.

Підтримувані формати читання метаданих: 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

Підтримувані формати запису метаданих: azw, azw1, azw3, azw4, docx, epub, fb2, fbz, htmlz, lrf, mobi, odt, pdb, pdf, prc, rtf, tpz, txtz

Для різних типів файлів передбачено різні метадані. Якщо ви спробуєте встановити метадані для файла, для типу якого ці метадані не передбачено, метадані буде просто відкинуто.

Якщо ви передаєте аргументи ebook-meta, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--author-sort

Рядок, що використовується для сортування за автором. Якщо не вказано, а авторів вказано, дані буде визначено за даними щодо авторів.

--authors, -a

Вкажіть авторів. Якщо авторів декілька, їхні імена слід розділити символом &. Імена авторів слід вказувати у форматі «Ім’я Прізвище».

--book-producer, -k

Вказати того, ким було створено книгу.

--category

Вказати категорію книги.

--comments, -c

Встановити опис електронної книги.

--cover

Визначити обкладинку для вказаного файла.

--date, -d

Вказати дату публікацію.

--from-opf

Прочитати метадані зі вказаного файла OPF і використовувати їх, як метадані для книги. Метадані, які вказано у рядку команди, замінять метадані, які прочитано з файла OPF.

--get-cover

Отримати обкладинку з книги і зберегти її у вказаному файлі.

--help, -h

показати це довідкове повідомлення і завершити роботу

--identifier

Встановити ідентифікатори книги. Можна вказувати декілька таких параметрів. Приклад: --identifier uri:https://acme.com --identifier isbn:12345 Щоб вилучити ідентифікатор, вкажіть порожнє значення. Приклад: --identifier isbn: Зауважте, що у файлах EPUB вилучення ідентифікатора, позначеного як ідентифікатор пакунка, неможливе.

--index, -i

Вказати номер книги в циклі.

--isbn

Вказати ISBN книги.

--language, -l

Встановити мову.

--lrf-bookid

Вказати BookID в LRF файлах

--publisher, -p

Встановити запис видавця електронної книги.

--rating, -r

Вказати оцінку. Може бути число між 1 і 5.

--series, -s

Вказати цикл, до якого належить книжка.

--tags

Вказати мітки для книги. Повинні бути розділені комами.

--title, -t

Вказати заголовок

--title-sort

Версія заголовка, що використовується для сортування. Якщо не вказано, а заголовок вказано, дані буде автоматично визначено на основі заголовка.

--to-opf

Вкажіть назву файла OPF. Метадані будуть записані в OPF файлі.

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- ebook-polish

ebook-polish

ebook-polish [параметри] файл_вхідних_даних [файл_результатів]

Покращення книг — це процедура, подібна до полірування дорогоцінного каміння, до якого подібні ретельно зібрані книги

Під час покращення програмні інструменти намагатимуться мінімізувати зміни, внесені до коду електронної книги
На відміну від перетворення даних, не відбуватиметься спрощення CSS, перейменування файлів, зміни розміну шрифтів, коригування полів тощо. Всі дії буде зведено до мінімуму, який потрібен для отримання бажаного результату.

Цим інструментом слід користуватися на останньому кроці створення вашої електронної книги.

Зауважте, що покращення працює лише для файлів у форматах AZW3 або EPUB.

Якщо ви передаєте аргументи ebook-polish, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--add-soft-hyphens, -H

Додати м'які переноси до усіх слів у книзі. Такі переноси поліпшують вигляд книг із вирівнюванням тексту за шириною сторінки на засобах для читання, де не передбачено автоматичного перенесення слів.

--compress-images, -i

Без втрати стиснути зображення у книзі для зменшення розміру вайла без втрати якості зображень.

--cover, -c

Шлях до зображення обкладинки Цим файлом буде замінено обкладинку, визначену у книзі. Якщо обкладинки немає або обкладинку не визначено належним чином, вставляє нову обкладинку.

--embed-fonts, -e

Вбудувати усі шрифти, які використано у документі і ще не вбудовано до нього. Програма виконає пошук шрифтів на вашому комп’ютері і, якщо шрифти буде знайдено, вбудує їх до документа. Будь ласка, зважте на умови ліцензування, пов’язані з вбудовуванням шрифтів до цього документа.

--help, -h

показати це довідкове повідомлення і завершити роботу

--jacket, -j

Вставити сторінку «суперобкладинки» на початку книги. Ця сторінка міститиме всі метадані книги, зокрема назву, мітки авторів, цикл, коментарі тощо. Всі попередні параметри «суперобкладинки» буде збережено.

--opf, -o

Шлях до файла OPF. Метадані у книзі оновлено на основі даних з файла OPF.

--remove-jacket

Вилучити попередньо вставлену сторінку суперобкладинки.

--remove-soft-hyphens

Вилучити м'які переноси з усього тексту книги.

--remove-unused-css, -u

Вилучити усі невикористані правила CSS з таблиць стилів та теґів <style>. У деяких книгах, створених на основі шаблонів, багато зайвих правил CSS, які не використовуються для форматування даних. Ці правила можуть уповільнити обробку програмами для читання.

--smarten-punctuation, -p

Виконати перетворення звичайних дефісів, багатокрапок, тире з декількох дефісів тощо відповідно до типографських стандартів. Зауважте, що використання цього алгоритму може іноді призводити до некоректних результатів, особливо якщо на початку фрагментів тексту використано одинарні лапки.

--subset-fonts, -f

Обрізання шрифтів до підмножин означає, що у вбудованому шрифті міститимуться лише символи, які використано у книзі. За допомогою такого обрізання можна значно зменшити розмір файлів шрифтів (типовим є зменшення приблизно удвічі). Наприклад, якщо у книзі використано специфічний шрифт для заголовків, обрізання призведе до того, що у вбудованому шрифті залишаться дані лише щодо символів, які було використано у заголовках книги. Якщо ж у книгу вбудовано напівжирну та курсивну версії шрифту, але напівжирне та курсивне написання трапляється у ній доволі нечасто або його і взагалі немає, шрифти для напівжирного та курсивного тексту може бути зменшено до декількох символів або і повністю вилучено. Єдиною незручністю, яка може трапитися після обрізання шрифтів є те, що якщо пізніше ви захочете додати текст до книги деяких його символів може не бути у обрізаній підмножині вбудованого шрифту.

--upgrade-book, -U

Оновити внутрішню структуру книги, якщо це можливо. Наприклад, можна оновити книги EPUB 2 до версії формату EPUB 3.

--verbose

Вивести докладніші повідомлення, корисно для діагностування помилок.

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- ebook-viewer

ebook-viewer

ebook-viewer [параметри] файл

Переглянути електронну книгу

Якщо ви передаєте аргументи ebook-viewer, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--continue

Продовжити читання останньої відкритої книги

--detach

Від’єднати від контрольного термінала, якщо такий був (лише Linux)

--force-reload

Примусово перезавантажити усі відкриті книги

--full-screen, --fullscreen, -f

Якщо вказано, буде виконано спробу відкрити вікно перегляду на весь екран під час запуску.

--help, -h

показати це довідкове повідомлення і завершити роботу

--new-instance

Відкрити нове вікно перегляду книги, навіть якщо програму налаштовано на використання одного вікна перегляду книг

--open-at

Місце, на якому слід відкрити вказану книгу. Позначка місця визначає розташування у тексті позиції, до якої буде здійснено перехід при використанні дії «Перейти до -> Місце» на панелі керування переглядом. Крім того, ви можете скористатися формою toc:щось, щоб відкрити місце у першій таблиці змісту, яке містить рядок «щось». Форма toc-href:щось відповідає href (внутрішній прив'язці посилання) вузлів таблиці змісту. Відповідність має бути повною. Якщо вам потрібна відповідність частині рядка, скористайтеся формою toc-href-contains:щось. Якщо скористатися формою «ref:посилання», програма виконає пошук у режимі посилань. Якщо скористатися формою search:щось, буде виконано пошук рядка «щось» після відкриття книги. Якщо скористатися формою regex:щось, буде виконано пошук за формальним виразом після відкриття книги.

--raise-window

Якщо вказано, буде виконано спробу перевести вікно перегляду на передній план під час запуску.

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- fetch-ebook-metadata

fetch-ebook-metadata

fetch-ebook-metadata [параметри]

Отримати метадані книги із джерел у інтернеті. Вам слід вказати назву, авторів або ISBN.

Якщо ви передаєте аргументи fetch-ebook-metadata, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--allowed-plugin, -p

Вказати назву додатка отримання метаданих. Типово, використовуються усі додатки метаданих. Можна вказувати декілька разів, щоб визначити декілька додатків. Назви усіх додатків: Google, Google Images, Amazon.com, Edelweiss, Open Library, Big Book Search

--authors, -a

Автори книги

--cover, -c

Вказати назву файла. Зображення обкладинки, якщо таке буде виявлено, буде збережено до файла із вказаною назвою. Якщо параметр не вказано, програма не робитиме спроб отримати обкладинку.

--help, -h

показати це довідкове повідомлення і завершити роботу

--identifier, -I

Ідентифікатори, зокрема ідентифікатори ASIN/Goodreads. Можна вказувати декілька разів, якщо ідентифікаторів декілька. Приклад: --identifier asin:B0082BAJA0

--isbn, -i

ISBN книги

--opf, -o

Вивести метадані у форматі OPF, а не у текстовому форматі, зручному для читання.

--timeout, -d

Час очікування у секундах. Типовим є значення 30.

--title, -t

Назва книги

--verbose, -v

Вивести повідомлення журналу до консолі (stderr).

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- lrf2lrs

lrf2lrs

lrf2lrs book.lrf

Перетворює LRF файл у LRS файл (XML UTF-8 кодування)

Якщо ви передаєте аргументи lrf2lrs, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--dont-output-resources

Не зберігати вкладені зображення і шрифти на диск

--help, -h

показати це довідкове повідомлення і завершити роботу

--output, -o

Вихідний LRS файл

--verbose

Режим з докладним виводом

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- lrfviewer

lrfviewer

lrfviewer [параметри] книга.lrf

Прочитати електронну книгу LRF книга.lrf

Якщо ви передаєте аргументи lrfviewer, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--disable-hyphenation

Вимкнути перенесення рядків. Має значно пришвидшити показ вмісту книги.

--help, -h

показати це довідкове повідомлення і завершити роботу

--profile

Профілювати засіб обробки LRF

--verbose

Вивести додаткову інформацію щодо процесу обробки для показу

--version

показати дані щодо версії програми і завершити роботу

--visual-debug

Увімкнути візуальні підказки для діагностики рушія показу даних

--white-background

Типово тло є сіруватим, оскільки автор програми вважає його зручнішим для читання. За допомогою цього параметр можна зробити тло білим.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- lrs2lrf

lrs2lrf

lrs2lrf [options] file.lrs

Укладання файла LRS з файла LRF.

Якщо ви передаєте аргументи lrs2lrf, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--help, -h

показати це довідкове повідомлення і завершити роботу

--lrs

Перетворення LRS в LRS, корисно для діагностики.

--output, -o

Шлях для вихідного файла

--verbose

Деталізація процесу

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Інтерфейс командного рядка »
- web2disk

web2disk

web2disk адреса

Приклад адреси: https://google.com

Якщо ви передаєте аргументи web2disk, які містять пробіли, додайте навколо таких аргументів лапки. Приклад: «/some path/with spaces»

[параметри]

--base-dir, -d

Базова тека, до якої зберігатиметься адреса. Типовою є .

--delay

Мінімальний інтервал (у секундах) між послідовними запитами. Типово 0 с.

--dont-download-stylesheets

Не завантажено CSS стилі.

--encoding

Кодування для веб-сайтів, які ви намагаєтеся завантажити. Типово кодування вгадується.

--filter-regexp

Всі посилання, які відповідають цьому формальному виразу, буде проігноровано. Цей параметр можна вказувати декілька разів. У такому разі кожного разу, коли формальний вираз відповідатиме посиланню, посилання буде проігноровано. За типових умов не ігноруватиметься жодне посилання. Якщо буде вказано формальний вираз фільтрування і формальний вираз відповідності, першим буде застосовано формальний вираз фільтрування.

--help, -h

показати це довідкове повідомлення і завершити роботу

--match-regexp

Тільки посилання, які відповідають цьому формальному виразу будуть залишатись. Цей параметр може бути заданий кілька разів, і в цьому випадку, якщо посилання відповідає будь-якому з формальних виразів, воно залишиться. Типово всі посилання залишаються.

--max-files, -n

Максимальна кількість файлів для отримання. Застосовується лише до файлів з міток <a href>. Типово 9223372036854775807

--max-recursions, -r

Максимальна кількість рівнів рекурсії, тобто глибина переходу за посиланнями. Типовим є значення 1

--timeout, -t

Час очікування у секундах на відповідь від сервера. Типове значення: 10.0 с

--verbose

Показувати докладні виведені дані. Корисно для діагностики.

--version

показати дані щодо версії програми і завершити роботу

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »

- Налаштовування середовища для розробки calibre

Налаштовування середовища для розробки calibre

Увесь початковий код calibre відкрито і ліцензовано відповідно до GNU GPL v3 [https://www.gnu.org/licenses/gpl.html]. Це означає, що ви можете безкоштовно отримувати код програми і вносити до нього будь-які потрібні вам зміни. За допомогою цього розділу ви зможете ознайомитися із тим, як створити середовище для розробки calibre у вашій операційній системі. calibre, в основному, написано мовою програмування Python [https://www.python.org] із вставками мовою програмування C/C++ для підвищення швидкодії та роботи із системою. Зауважте, що програма calibre є потребує для роботи принаймні Python 3.8.

Зміст

- Засади структури програми

- Структура коду

- Отримання коду

- Надсилання ваших змін для включення до програми

- Середовище для розробки у Windows

- Середовище розробки у macOS

- Середовище розробки у Linux

- Використання окремих «звичайного» і «розробницького» встановлень calibre на одному комп’ютері

- Підказки щодо діагностики

- Використання інструкцій print

- Використання інтерактивного інтерпретатора Python

- Використання засобу діагностики Python як віддалений засіб діагностики

- Використання засобу діагностики у вашому улюбленому комплексному середовищі розробки Python

- Виконання довільних скриптів у середовищі Python calibre

- Використання calibre у ваших проектах

- Зі встановленого бінарного пакунка calibre

- Зі встановлених початкових кодів у Linux

- Документація з програмного інтерфейсу до різноманітних частин calibre

Засади структури програми
calibre походить зі світу Unix, і це означає, що значна частина можливостей програми винесена у модулі. Ці модулі взаємодіють між собою за допомогою добре визначених інтерфейсів. Це робить додавання нових можливостей та виправлення вад у calibre дуже простим і надає розвитку програми шаленого темпу. Через своє походження calibre має всеосяжний інтерфейс командного рядка для усіх свої функціональних можливостей. Документацію з нього можна знайти у розділі Інтерфейс командного рядка.

Модульність calibre реалізується додатками. У нашому підручнику є розділ щодо написання додатків до calibre. Наприклад, додавання підтримки нового пристрою у calibre типово полягає у написанні менш ніж 100 рядків коду у формі додатка драйвера пристрою. Ви можете ознайомитися зі списком вбудованих драйверів [https://github.com/kovidgoyal/calibre/tree/master/src/calibre/devices]. Так само, додавання підтримки нових форматів перетворення полягає у написанні додатків форматів введення і виведення даних. Іншим прикладом модульної побудови є система рецептів для отримання новин. З іншими прикладами додатків, розроблених для розширення можливостей calibre, можна ознайомитися тут [https://www.mobileread.com/forums/showthread.php?p=1362767#post1362767].

Структура коду

Увесь код мовою Python calibre є частиною пакунка «calibre». У цьому пакунку містяться вказані нижче підпакунки

- devices — усі драйвери до пристроїв. Достатньо ознайомитися з кодом вбудованих до програми драйверів, щоб зрозуміти основні принципи роботи драйверів.

- Докладніший опис можна знайти у devices.interface, визначенні інтерфейсу підтримки драйверів пристроїв, та у devices.usbms, визначенні типового драйвера, який встановлює з’єднання із пристроєм USBMS. Усі драйвери на основі USBMS у calibre успадковуються від цього драйвера.

- ebooks — увесь код, який пов’язано із перетворенням та метаданими електронних книг. Варто розпочати з calibre.ebooks.conversion.cli, модуля, який забезпечує роботу програми ebook-convert. Процесом перетворення керує conversion.plumber. Незалежний від форматів код зберігається у ebooks.oeb, а залежний від форматів — у ebooks.format_name.

- Засоби читання, записування та отримання метаданих зосереджено у ebooks.metadata

- Перетворення виконується у конвеєрі. Зі структурою конвеєра можна ознайомитися у розділі Вступ. Конвеєр складається із додатка вхідних даних, різноманітних перетворено та додатка вихідних даних. Код, який будує конвеєр і керує ним, зберігається у plumber.py. Конвеєр працює над створенням представлення книги у формі розпакованого файла epub, із маніфестом manifest, вмістом, змістом, путівником, даними html тощо. Класом, який керує цим представленням, є клас OEBBook у ebooks.oeb.base. Різноманітні перетворення, які застосовуються до книги під час обробки, зберігаються у oeb/transforms/*.py. Код додатків вхідних і вихідних даних зберігається у conversion/plugins/*.py.

- Редагування електронних книг відбувається за допомогою іншого об’єкта-контейнера. Його документовано у розділі Документація з програмного інтерфейсу засобів редагування електронних книг.

- db — модуль бази даних. Див. розділ Документація з програмного інтерфейсу для роботи з базою даних, щоб дізнатися більше про інтерфейс бібліотеки calibre.

- Сервер даних: «srv» — це сервер даних calibre.

- gui2 — графічний інтерфейс користувача. Ініціалізація інтерфейсу виконується у gui2.main і gui2.ui. Сам засіб перегляду електронних книг міститься у gui2.viewer. За редагування електронних книг відповідає gui2.tweak_book.

Якщо вам потрібен доступ до усіх виконуваних файлів calibre, ознайомтеся зі структурою entry_points у linux.py [https://github.com/kovidgoyal/calibre/blob/master/src/calibre/linux.py].

Якщо хочете краще зрозуміти код, створіть тему із вашим питанням на форумі розробників [https://www.mobileread.com/forums/forumdisplay.php?f=240]. Найімовірніше, там ви отримаєте кваліфіковану відповідь від одного з багатьох розробників calibre.

Отримання коду

Отримати початковий код calibre можна у два способи: за допомогою системи керування версіями та безпосереднього отримання архіву з кодом програми [https://calibre-ebook.com/dist/src].

Для керування версіями у calibre використовують Git [https://www.git-scm.com/], розподілену систему керування версіями. Git можна користуватися на усіх платформах, на яких працює calibre. Після встановлення Git ви зможете отримати початковий код calibre за допомогою такої команди:

git clone https://github.com/kovidgoyal/calibre.git

У Windows вам знадобиться шлях повністю, щось схоже на C:\Program Files\Git\git.exe.

calibre є дуже великим проектом із дуже довгою історією, отже виконання вказаної вище команди може бути доволі тривалим (від 10 хвилин до години, залежно від пропускної здатності вашого каналу з’єднання).

Якщо ви хочете отримати код швидше, початковий код найсвіжішого випуску завжди можна отримати у форматі архіву [https://calibre-ebook.com/dist/src].

Щоб оновити гілку до актуального стану, скористайтеся такою командою:

git pull --no-edit

Ви також можете переглядати код на GitHub [https://github.com/kovidgoyal/calibre].

Надсилання ваших змін для включення до програми

Якщо ви плануєте внести лише незначні зміни, ви можете створити з них «інструкцію об’єднання», а потім долучити її до квитка у системі стеження за вадами [https://bugs.launchpad.net/calibre] calibre. Процедура така: вносимо зміни, а потім віддаємо такі команди:

git commit -am "Comment describing your changes"
git format-patch origin/master --stdout > my-changes

У результаті у поточній теці буде створено файл my-changes, який слід просто долучити до квитка у системі стеження за вадами [https://bugs.launchpad.net/calibre] calibre. Зауважте, що до латки буде включено усі внесені вами зміни. Якщо ви хочете надіслати лише частину змін, вам слід замінити origin/master у наведених вище командах. Щоб надіслати лише останню зміну, скористайтеся такою командою:

git format-patch HEAD~1 --stdout > my-changes

Щоб надіслати останні n внесків, замініть 1 на n. Наприклад, для 3 внесків, ось так:

git format-patch HEAD~3 --stdout > my-changes

Будьте обережні: не включайте об’єднання, якщо користуєтеся HEAD~n.

Якщо ви плануєте значний розвиток коду calibre, вам варто створити обліковий запис на GitHub [https://github.com]. Нижче наведено базові настанови щодо створення власної копії коду calibre у спосіб, який надасть вам змогу надсилати запити щодо об’єднання (pull requests) для включення вашого коду до основного сховища коду calibre:

- Налаштуйте git на вашому комп’ютері так, як це описано у цій статті: Setup Git [https://help.github.com/articles/set-up-git]

- Налаштуйте ключі ssh для розпізнавання на GitHub, як це описано тут: Generating SSH keys [https://help.github.com/articles/generating-ssh-keys]

- Перейдіть на сторінку https://github.com/kovidgoyal/calibre і натисніть кнопку Fork.

- У терміналі віддайте команди:

git clone git@github.com:<username>/calibre.git
git remote add upstream https://github.com/kovidgoyal/calibre.git

Замініть <ім’я_користувача> у наведеній вище команді на ваше ім’я користувача GitHub. Таким чином, ви отримаєте копію клонованого сховища на вашому комп’ютері.

- Ви можете внести зміни і надіслати їх будь-коли. Коли ви будете готові до внесення вашої роботи, зробіть от що:

git push

перейдіть на сторінку https://github.com/<ім’я користувача>/calibre і натисніть кнопку Pull Request, щоб створити запит на об’єднання.

- Ви будь-коли можете оновити вашу локальну копію кодом із основного сховища такою командою:

git pull upstream

Вам також варто слідкувати за форумом розробників [https://www.mobileread.com/forums/forumdisplay.php?f=240] calibre. Перш ніж вносити значні зміни, вам слід обговорити їх на форумі або безпосередньо зв’язатися з Ковідом (його адреса електронної пошти є у багатьох файлах початкового коду програми).

Середовище для розробки у Windows

Примітка

Вам слід також окремо отримати початковий код calibre, як це було описано вище.

Встановіть calibre у звичний спосіб, за допомогою пакунка для встановлення у Windows [https://calibre-ebook.com/download_windows]. Далі, відкрийте вікно командного рядка і перейдіть до отриманої теки з кодом calibre. Приклад:

cd C:\Users\kovid\work\calibre

calibre — тека, що містить підкаталоги src і resources.

Наступним кроком буде встановлення для змінної середовища CALIBRE_DEVELOP_FROM значення, яке буде абсолютним шляхом до теки src. Отже, якщо продовжувати наведений вище приклад, слід вказати шлях C:\Users\kovid\work\calibre\src. Тут [https://docs.python.org/using/windows.html#excursus-setting-environment-variables] наведено короткі настанови щодо налаштовування змінних середовища у Windows.

Після встановлення змінної середовища відкрийте нове вікно командного рядка і перевірте, чи все працює, за допомогою такої команди:

echo %CALIBRE_DEVELOP_FROM%

Встановлення цієї змінної середовища означає, що calibre тепер буде завантажувати увесь свій код Python із вказаного місця.

Ось і все! Тепер ви готові до роботи з кодом calibre. Наприклад, відкрийте файл src\calibre\__init__.py у вашому улюбленому редакторі і додайте такий рядок:

print("Hello, world!")

на початку файла. Тепер віддайте команду calibredb. Найпершим рядком у виведених даних має бути Hello, world!.

Ви також можете налаштувати середовище розробки calibre у вільній версії Microsoft Visual Studio, якщо хочете, виконавши настанови, викладені тут [https://www.mobileread.com/forums/showthread.php?t=251201].

Середовище розробки у macOS

Примітка

Вам слід також окремо отримати початковий код calibre, як це було описано вище.

Встановіть calibre у звичний спосіб, за допомогою пакунка для встановлення .dmg [https://calibre-ebook.com/download_osx]. Далі, відкрийте вікно термінала і перейдіть до отриманої теки з кодом calibre Приклад:

cd /Users/kovid/work/calibre

calibre — тека, у якій містяться підкаталоги src та resources. Засоби командного рядка calibre зберігаються у пакунку програми calibre, а саме, у /Applications/calibre.app/Contents/MacOS. Вам слід додати цю теку до вашої змінної середовища PATH, якщо ви маєте намір часто користуватися цими засобами командного рядка.

Наступним кроком є створення скрипту bash, який визначити для змінної середовища CALIBRE_DEVELOP_FROM значення абсолютного шляху до теки src під час запуску calibre у режимі діагностики.

Створіть звичайний текстовий файл:

#!/bin/sh
export CALIBRE_DEVELOP_FROM="/Users/kovid/work/calibre/src"
calibre-debug -g

Збережіть цей файл як :file`/usr/bin/calibre-develop`. Далі, встановіть права доступу так, щоб його можна було виконувати:

chmod +x /usr/local/bin/calibre-develop

Щойно ви це зробите, віддайте команду:

calibre-develop

Ви маєте побачити певну діагностичну інформацію у вікні термінала під час запуску calibre, і ви маєте побачити зірочку після номеру версії у вікні графічного інтерфейсу, що означає, що програму запущено із початкових кодів.

Середовище розробки у Linux

Примітка

Вам слід також окремо отримати початковий код calibre, як це було описано вище.

Основним середовищем розробки calibre є Linux. У вас є два варіанти налаштовування середовища для розробки. Ви можете встановити виконуваний файл calibre у звичний спосіб і скористатися цим інтерактивним середовищем для розробки. Цей підхід подібний до використаного у Windows і macOS. Крім того, ви можете встановити calibre з початкових кодів. Настанови щодо налаштовування середовища для розробки з початкових кодів наведено у файлі INSTALL дерева початкових кодів програми. Тут ми обговоримо використання середовища з бінарними файлами, тобто рекомендованого методу.

Встановіть calibre за допомогою бінарного пакунка для встановлення [https://calibre-ebook.com/download_linux]. Далі, відкрийте вікно термінала і перейдіть до отриманої теки з кодом calibre Приклад:

cd /home/kovid/work/calibre

calibre — тека, що містить підкаталоги src і resources.

Наступним кроком є встановлення для змінної середовища CALIBRE_DEVELOP_FROM значення абсолютного шляху до теки src. Отже, продовжуючи наведений вище приклад, маємо встановити значення /home/kovid/work/calibre/src. Спосіб встановлення значення для змінної середовища залежить від вашого дистрибутива Linux та використаної вами командної оболонки.

Примітка

Рекомендуємо скористатися двійковим пакунком для встановлення, який надають розробники програми. Якщо все ж хочете скористатися пакунком з вашого дистрибутиву, скористайтеся змінними CALIBRE_PYTHON_PATH та CALIBRE_RESOURCES_PATH.

Після встановлення змінної середовища відкрийте новий термінал і перевірте, чи все працює, за допомогою такої команди:

echo $CALIBRE_DEVELOP_FROM

Встановлення цієї змінної середовища означає, що calibre тепер буде завантажувати увесь свій код Python із вказаного місця.

Ось і все! Тепер ви готові до роботи з кодом calibre. Наприклад, відкрийте файл src/calibre/__init__.py у вашому улюбленому редакторі і додайте такий рядок:

print("Hello, world!")

на початку файла. Тепер віддайте команду calibredb. Найпершим рядком у виведених даних має бути Hello, world!.

Використання окремих «звичайного» і «розробницького» встановлень calibre на одному комп’ютері

Початкові коди calibre є доволі стабільними, критичні помилки у них є дуже рідкісним явищем, але якщо ви хочете запустити програму з початкових кодів на окремій тестовій бібліотеці і користуватися стабільною версією calibre для вашої справжньої бібліотеки, досягти цього можна за допомогою запуску calibre з файлів .bat або скриптів оболонки. У наведеному нижче прикладі показано, як досягти цього у Windows за допомогою файлів .bat (настанови для інших платформ дуже подібні, але у них використовуватиметься скрипт командної оболонки, а не файл .bat)

Щоб запустити версію для випуску calibre з вашою щоденною бібліотекою, зробіть так:

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"

Підказки щодо діагностики

Python — мова з динамічною типізацією та чудовими можливостями з інтроспекції. Ковід створив код основних бібліотек calibre без жодного застосування засобів діагностики. Втім, існує багато стратегій діагностування коду calibre:

Використання інструкцій print

Цей улюблений спосіб діагностування Ковіда. Просто вставте інструкції print у потрібні вам місця у коді і запустіть програму з термінала. Наприклад, ви можете запустити графічний інтерфейс програми з термінала ось так:

calibre-debug -g

Так само, ви можете запустити засіб для перегляду електронних книг ось так:

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

Використання інтерактивного інтерпретатора Python

Ви можете вставити такі два рядки коду, щоб запустити інтерактивний сеанс Python на цьому кроці:

from calibre import ipython
ipython(locals())

Якщо програму запущено з командного рядка, цей код запустити інтерактивний інтерпретатор Python із доступом до усіх локально визначених змінних (змінних у області local). У інтерактивному командному рядку передбачено навіть автоматичне доповнення за натисканням клавіші Tab властивостей об’єктів. Ви також можете скористатися різноманітними можливостями Python для інтроспекції, зокрема dir(), type(), repr() тощо.

Використання засобу діагностики Python як віддалений засіб діагностики

Ви можете скористатися вбудованим засобом діагностики Python (pdb) як віддаленим засобом діагностики з командного рядка. Спочатку, запустіть віддалений засіб діагностики у точці коду calibre, яка вам потрібна, ось так:

from calibre.rpdb import set_trace
set_trace()

Далі, запустіть calibre у звичний спосіб або за допомогою команд calibre-debug, які описано у попередньому розділі. Щойно буде досягнуто визначеної вище точки у коді, роботу calibre буде призупинено, вона очікуватиме на з’єднання із засобом діагностики.

Тепер відкрийте вікно термінала або командний рядок і віддайте таку команду, щоб розпочати сеанс діагностики:

calibre-debug -c "from calibre.rpdb import cli; cli()"

Дізнатися більше про те, як користуватися засобом діагностики python, можна за допомогою документації щодо стандартної бібліотеки Python, модуль pdb [https://docs.python.org/library/pdb.html#debugger-commands].

Примітка

Типово, засіб віддаленої діагностики намагатиметься встановити з’єднання на порту 4444. Ви можете змінити порт, передавши параметр порту до функцій set_trace() і cli(), ось так: set_trace(port=1234) і cli(port=1234).

Примітка

Засіб діагностики Python не може працювати одночасно з декількома потоками виконання, тому вам слід викликати set_trace для кожного потоку, кожного разу на портах із різними номерами.

Використання засобу діагностики у вашому улюбленому комплексному середовищі розробки Python

Можна скористатися вбудованим засобом діагностики у вашому улюбленому комплексному середовищі для розробки, якщо там передбачено віддалену діагностику. Першим кроком є додавання адреси клону src calibre до змінної PYTHONPATH у вашому комплексному середовищі для розробки. Іншими словами, тека, яку ви визначили для змінної CALIBRE_DEVELOP_FROM вище, має також бути серед тек у змінній PYTHONPATH вашого комплексного середовища для розробки.

Далі, розташуйте модуль віддаленого засобу діагностики комплексного середовища розробки у підтеці src клону початкового коду calibre. Додайте код, який потрібен для запуску віддаленого засобу діагностики у код calibre у потрібному вам місці, наприклад у функції main. Далі, запустіть calibre у звичний спосіб. Після цього ваше комплексне середовище розробки має з’єднатися із засобом віддаленої діагностики, запущеним у calibre.

Виконання довільних скриптів у середовищі Python calibre

У програмі calibre-debug передбачено декілька зручних перемикачів для виконання вашого власного коду із доступом до модулів calibre:

calibre-debug -c "some Python code"

чудово пасує для тестування невеличкого фрагмента коду у командному рядку. Працює так само, як перемикач -c самого інтерпретатора Python:

calibre-debug myscript.py

може бути використано для запуску вашого власного скрипту мовою Python. Працює так само, як передавання скрипту інтерпретатору Python, але із повністю ініціалізованим середовищем calibre. Отже, ви зможете скористатися усім кодом calibre у вашому скрипті. Щоб передати скрипту параметри командного рядка, скористайтеся таким форматом:

calibre-debug myscript.py -- --option1 arg1

Додавання -- спричинить передавання усіх наступних аргументів до вашого скрипту.

Використання calibre у ваших проектах

Ви можете безпосередньо скористатися функціями або кодом calibre у вашому проекті мовою Python. Передбачено два способи зробити це:

Зі встановленого бінарного пакунка calibre

Якщо вами встановлено бінарний пакунок calibre, ви можете скористатися інтерпретатором Python, який міститься у ньому. Ось так:

calibre-debug /path/to/your/python/script.py -- arguments to your script

Зі встановлених початкових кодів у Linux

Окрім використання описаної вище методики, якщо ви встановили програму з початкових кодів у Linux, ви також можете безпосередньо імпортувати модуль calibre, ось так:

import init_calibre
import calibre

print(calibre.__version__)

Дуже важливо імпортувати модуль init_calibre до будь-яких інших модулів або пакунків calibre, оскільки цей модуль налаштовує інтерпретатор на виконання коду calibre.

Документація з програмного інтерфейсу до різноманітних частин calibre

- Документація з програмного інтерфейсу для роботи з рецептами

- Документація з програмного інтерфейсу для роботи з додатками

- Документація з програмного інтерфейсу для роботи з базою даних

- Документація з програмного інтерфейсу засобів редагування електронних книг

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »

- Налаштовування середовища для розробки calibre »

- Документація з програмного інтерфейсу для роботи з базою даних

Документація з програмного інтерфейсу для роботи з базою даних

Цей програмний інтерфейс безпечний щодо потоків виконання (використовує схему із декількома потоками читання і одним потоком запису із блокуванням). Доступ до програмного інтерфейсу можна отримати так:

from calibre.library import db
db = db('Path to calibre library folder').new_api

Якщо ви працюєте із додатком до calibre, який є частиною основного графічного інтерфейсу calibre, доступ можна отримати ось так:

db = self.gui.current_db.new_api

class calibre.db.cache.Cache(backend, library_database_instance=None)[source]

Кеш у пам’яті файла metadata.db з бібліотеки calibre. Цей клас також слугує безпечним для потоків обробки програмним інтерфейсом для доступу до бази даних. Кеш у пам’яті підтримується у нормальній формі для збільшення швидкодії.

SQLITE просто використовується як стійкий спосіб читання і запису metadata.db. Усю логіку читання, упорядковування, пошуку та кешування таблиць реалізовано окремо. Так зроблено для забезпечення максимальної швидкодії і гнучкості.

class EventType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

book_created = 4

Коли у базі даних буде створено новий запис книги із ідентифікатором книги, як єдиним аргументом

book_edited = 8

Коли буде змінено формат книги з аргументами: (ідентифікатор_книги, формат)

books_removed = 5

Коли книги буде вилучено з бази даних із списком ідентифікаторів книг, як єдиним аргументом

format_added = 2

Коли до запису книги буде додано формат із аргументами: (ідентифікатор_книги, форма)

formats_removed = 3

Коли формати буде вилучено з запису книги з аргументами: (відображення ідентифікатора книги на формати, які вилучено з запису книги)

indexing_progress_changed = 9

Коли змінюється поступ індексування

items_removed = 7

Коли з деяких книг вилучають записи, подібні до міток або записів авторів. Аргументи: (назва_поля, ідентифікатори книг, ідентифікатори записів, які вилучають)

items_renamed = 6

Коли у деяких або усіх книгах перейменовують записи, подібні до міток або записів авторів. Аргументи: (назва_поля, ідентифікатори книг, відображення старого ідентифікатора запису на новий ідентифікатор запису)

metadata_changed = 1

Коли змінюють деякі метадані у деяких книгах з аргументами: (назва зміненого поля, набір ідентифікаторів книг)

add_books(books, add_duplicates=True, apply_import_tags=True, preserve_uuid=False, run_hooks=True, dbapi=None)[source]

Додати вказані книги до бібліотеки. Books має бути ітерованим списком з двоелементних кортежів, кожен кортеж має бути записано у формі (mi, format_map), де mi — об’єкт Metadata, а format_map — словник у формі form {формат: шлях_або_потік}. Приклад: {'EPUB': '/шлях/до/файла.epub'}.

Повертає пару списків: ids, duplicates. ids містить ідентифікатори усіх новостворених записів книг у базі даних. duplicates містить записи (mi, format_map) для усіх книг, які вже є у базі даних, на основі простої евристики виявлення дублікатів, що використовується у has_book().

add_custom_book_data(name, val_map, delete_first=False)[source]

Додати дані для вказаної назви, де val_map — прив’язка book_id до значень. Якщо delete_first має значення True, усі раніше збережені для вказаної назви дані буде вилучено.

add_extra_files(book_id, map_of_relpath_to_stream_or_path, replace=True, auto_rename=False)[source]

Додати додаткові файли даних

add_format(book_id, fmt, stream_or_path, replace=True, run_hooks=True, dbapi=None)[source]

Додати запис формату до вказаного запису книги. Повертає True, якщо запис формату було успішно додано.

Параметри:

- replace – Якщо True, замінити наявний формат, інакше, якщо формат вже існує, повернути False.

- run_hooks – Якщо True, додатки форматів типів файлів опрацьовують формат до і після додавання.

- dbapi – Лише для внутрішнього використання.

add_listener(event_callback_function, check_already_added=False)[source]

Зареєструвати функцію зворотного виклику, яку буде викликано після певних дій над цією базою даних. Функція має приймати три аргументи: (EventType, ідентифікатор_бібліотеки, специфічні_дані_типу_події)

add_notes_resource(path_or_stream_or_data, name: str, mtime: float = None) → int[source]

Add the specified resource so it can be referenced by notes and return its content hash

all_book_ids(type=<class 'frozenset'>)[source]

Заморожений набір усіх відомих ідентифікаторів книг.

all_field_for(field, book_ids, default_value=None)[source]

Те саме, що і field_for, але одночасно обробляється декілька книг

all_field_ids(name)[source]

Заморожений набір ідентифікаторів для усіх значень у полі «name».

all_field_names(field)[source]

Заморожений набір усіх назв полів (слід використовувати лише для полів багато-одне і багато-багато)

author_data(author_ids=None)[source]

Повернути дані щодо авторів у форматі словника з такими ключами: name, sort, link

Якщо авторів із вказаними ідентифікаторами немає, буде повернуто порожній словник. Якщо author_ids має значення None, буде повернуто дані для усіх авторів.

author_sort_from_authors(authors, key_func=<function make_change_case_func.<locals>.change_case>)[source]

За вказаними списком авторів повертає рядок author_sort для авторів, надаючи перевагу упрядковуванню за авторами, пов’язаному з автором, над обчисленим рядком.

books_for_field(name, item_id)[source]

Повертає усі книги, пов’язані із записом, вказаними за допомогою item_id, де запис належить полю name.

Повернуте значення є набором ідентифікаторів книг або порожньою множиною, якщо запису або поля не існує.

books_in_virtual_library(vl, search_restriction=None, virtual_fields=None)[source]

Повертає набір книг у вказаній віртуальній бібліотеці

compress_covers(book_ids, jpeg_quality=100, progress_callback=None)[source]

Стиснути зображення обкладинок вказаних книг. Якість стискання 100 означає виконання стискання без втрати даних. Усі інші значення призводитимуть до втрати якості зображення.

Зворотний виклик поступу буде виконано із використанням ідентифікатора книги та старого і нового розмірів для кожної обробленої книги. Якщо станеться помилка, розміром новин буде рядок із повідомленням про помилку.

copy_cover_to(book_id, dest, use_hardlink=False, report_file_size=None)[source]

Копіювати зображення обкладинки до файлоподібного об’єкта dest. Повертає False, якщо зображення обкладинки не існує, або dest є тим самим файлом, що і поточне зображення обкладинки. dest також може бути шляхом. У випадку шляху зображення обкладинки буде скопійовано тоді і лише тоді, коли шлях відрізняється від поточного шляху (із врахуванням регістру символів).

copy_format_to(book_id, fmt, dest, use_hardlink=False, report_file_size=None)[source]

Копіювати формат fmt до файлоподібного об’єкта dest. Якщо вказаного формату не існує, повідомляє про помилку NoSuchFormat. dest може бути шляхом. Якщо dest є шляхом (до файла), формат буде скопійовано до нього тоді і лише тоді, коли шлях відрізняється від поточного шляху (із врахуванням регістру символів).

cover(book_id, as_file=False, as_image=False, as_path=False, as_pixmap=False)[source]

Повертає зображення обкладинки або None. Типово, повертає обкладинку як байтовий рядок.

ПОПЕРЕДЖЕННЯ: використання as_path призведе до копіювання зображення обкладинки до тимчасового файла і повернення шляху до цього тимчасового файла. Вам слід вилучити тимчасовий файл, коли ви завершите роботу з ним.

Параметри:

- as_file – Якщо True, повернути зображення як відкритий об’єкт файла (SpooledTemporaryFile)

- as_image – Якщо True, повернути зображення як об’єкт QImage.

- as_pixmap – Якщо True, повертає зображення як об’єкт QPixmap

- as_path – Якщо True, повернути зображення як шлях, що вказує на тимчасовий файл.

data_for_find_identical_books()[source]

Повертає дані, якими можна скористатися для реалізації find_identical_books() у робочому процесі без доступу до бази даних. Див. db.utils, щоб ознайомитися із реалізацією.

data_for_has_book()[source]

Повертає дані, придатні до використання у has_book(). Можна скористатися для реалізації has_book() у процесі обробки без доступу до бази даних.

delete_custom_book_data(name, book_ids=())[source]

Вилучити дані за вказаною назвою. Типово вилучає усі дані. Якщо вам потрібно вилучити дані лише для деяких ідентифікаторів книг, передайте список ідентифікаторів книг.

delete_trash_entry(book_id, category)[source]

Вилучити запис зі смітника. Тут категоріями є «b» для книг і «f» для форматів.

embed_metadata(book_ids, only_fmts=None, report_error=None, report_progress=None)[source]

Оновити метадані в усіх форматах вказаних book_id до поточних метаданих у базі даних.

expire_old_trash()[source]

Вилучити надто старі записи зі смітника

export_note(field, item_id) → str[source]

Export the note as a single HTML document with embedded images as data: URLs

fast_field_for(field_obj, book_id, default_value=None)[source]

Те саме, що і field_for, але з униканням додаткового пошуку для отримання об’єкта поля.

field_for(name, book_id, default_value=None)[source]

Повертає значення поля name для книги, яку вказано за допомогою ідентифікатора book_id. Якщо такої книги не існує, у поля name немає певного значення, або таке поле не існує, буде повернуто default_value.

default_value не використовується для полів title, title_sort, authors, author_sort та series_index. Так зроблено, оскільки ці поля завжди мають значення у базі даних. default_value використовується для усіх нетипових стовпчиків.

Повернутим значення для полів is_multiple завжди будуть кортежі, навіть якщо значень не буде знайдено (іншими словами, default_value буде проігноровано). Виключенням є ідентифікатори, для яких повернуте значення завжди буде словником. Кортежі, які буде повернуто, завжди упорядковуються за посиланнями, тобто порядком їхнього створення.

field_ids_for(name, book_id)[source]

Повертає ідентифікатори (у форматі кортежу) для значень, які поле name містить для книги, що визначається ідентифікатором book_id. Якщо значень немає, або немає такої книги, або немає такого поля, буде повернуто порожній кортеж.

field_supports_notes(field=None) → bool[source]

Return True iff the specified field supports notes. If field is None return frozenset of all fields that support notes.

find_identical_books(mi, search_restriction='', book_ids=None)[source]

Знаходить книги, набір авторів яких містить mi, і які мають ту саму назву title (відповідність title встановлюється неточно). Див. також data_for_find_identical_books().

format(book_id, fmt, as_file=False, as_path=False, preserve_filename=False)[source]

Повертає дані формату електронної книги у форматі байтового рядка або «None», якщо формату не існує або у вашого користувача немає права доступу до запису файла електронної книги.

Параметри:

- as_file – Якщо має значення True, дані формату електронної книги повертаються як файловий об’єкт. Зауважте, що файлових об’єкт належатиме до типу SpooledTemporaryFile. Отже, якщо ви хочете скопіювати формат до іншого файла, скористайтеся замість цієї функції функцією copy_format_to() з міркувань швидкодії.

- as_path – Копіює файл формату до тимчасового файла і повертає шлях до тимчасового файла.

- preserve_filename – Якщо має значення True і повертаємо шлях, назва файла збігається з використаною у бібліотеці назвою. Зауважте, що використання цього параметра означає, що повторні виклики даватимуть той самий тимчасовий файл (який лише кожного разу створюватиметься повторно).

format_abspath(book_id, fmt)[source]

Повертає абсолютний шлях до файла електронної книги формату format. Цим майже ніколи не варто користуватися, оскільки використання шкодить захисту потоків виконання цього програмного інтерфейсу. Замість цієї функції, скористайтеся copy_format_to().

У поточній версії використовується лише у списку calibredb, засобі перегляду, засобі редагування, compare_format для порівняння з початковим форматом, засобі відкриття файла за допомогою іншої програми, засобі пакетного редагування метаданих та у каталогах (через get_data_as_dict()).

Окрім засобів перегляду, відкриття за допомогою іншої програми та редагування книги, як здається авторові програми, жодна інша частина не виконує будь-яких дій із запису файла вхідних-вихідних даних із результатами цього виклику.

format_hash(book_id, fmt)[source]

Повертає хеш-суму вказаного формату вказаної книги. Тип модуля обробки для отримання сум може бути різним, але, зазвичай, це SHA-256.

format_metadata(book_id, fmt, allow_cache=True, update_db=False)[source]

Повертає шлях, розмір і mtime для вказаного формату вказаної книги. Вам не варто використовувати шлях, окрім безвихідних випадків, оскільки доступ до нього безпосередньо шкодить захисту виконання у потоках, який гарантується цим програмним інтерфейсом. Замість цієї функції, скористайтеся методом copy_format_to().

Параметри:

- allow_cache – Якщо має значення True, використовуються кешовані значення. У інших випадках використовується повільний доступ до файлової системи. Кешовані значення можуть втрачати актуальність, якщо доступ до файлової системи здійснюється поза цим програмним інтерфейсом.

- update_db – Якщо має значення True, поле бази даних max_size для цієї книги оновлено.

formats(book_id, verify_formats=True)[source]

Повертає кортеж усіх форматів для вказаної книги. Якщо verify_formats має значення True, перевіряє наявність файлів на диску.

get_all_items_that_have_notes(field_name=None) → set[int] | dict[str, set[int]][source]

Return all item_ids for items that have notes in the specified field or all fields if field_name is None

get_all_link_maps_for_book(book_id)[source]

Повертає усі посилання для усіх полів, які використано у книзі, на яку вказує ідентифікатор_книги. Якщо книги з вказаним ідентифікатором не існує, метод повертає {}.

Приклад: нехай запис автора A містить посилання X, а запис автора B містить посилання Y, мітка S містить посилання F, а мітка T містить посилання G. Якщо книга 1 має автора A і мітку T, цей метод поверне {„authors“:{„A“:“X“}, „tags“:{„T“, „G“}}. Якщо автором книги 2 не є ні A, ні B, і вона не має міток, цей метод поверне {}.

Параметри:

book_id – ідентифікатор потрібної книги.

Повертає:

{поле: {значення_поля, значення_посилання}, … для усіх полів, які містять значення_поля і непорожнє значення посилання для цієї книги

get_categories(sort='name', book_ids=None, already_fixed=None, first_letter_sort=False)[source]

Для внутрішньої реалізації панелі перегляду міток

get_custom_book_data(name, book_ids=(), default=None)[source]

Отримати дані для назви name. Типово, повертає дані для усіх book_ids. Передайте функції список ідентифікаторів книг, якщо вам потрібна лише якась частина даних. Повертає прив’язку book_id до значень. Якщо певне значення не вдасться декодувати, використовує типове значення.

get_id_map(field)[source]

Повертає прив’язку номерів ідентифікаторів до значень для вказаного поля. Поле має бути полем багато-одне або багато-багато, інакше буде надіслано повідомлення ValueError.

get_ids_for_custom_book_data(name)[source]

Повернути набір ідентифікаторів книг, для яких для вказаної назви є дані.

get_item_id(field, item_name)[source]

Повертає ідентифікатор запису для item_name (без врахування регістру) або None, якщо нічого не знайдено

get_item_ids(field, item_names)[source]

Повертає ідентифікатор запису для item_name (без врахування регістру)

get_item_name(field, item_id)[source]

Повертає назву запису для запису, вказаного за item_id, у вказаному полі. Див. також 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]

Повернути словник посилань для наданого поля.

Параметри:

for_field – пошукова назва поля, для якого потрібна карта посилань

Повертає:

{значення_поля:значення_посилання, …} для непорожніх посилань

get_metadata(book_id, get_cover=False, get_user_categories=True, cover_as_data=False)[source]

Повертає метадані для книги, заданої book_id, як об’єкт calibre.ebooks.metadata.book.base.Metadata. Зауважте, що список форматів не буде перевірено. Якщо get_cover має значення True, буде повернуто зображення обкладинки, як шлях до тимчасового файла у форматі mi.cover або, якщо cover_as_data має значення True, у форматі mi.cover_data.

get_next_series_num_for(series, field='series', current_indices=False)[source]

Повертає наступний індекс циклу для вказаного циклу, беручи до уваги різноманітні налаштування, які керують створенням наступного номера у книжковому циклі.

Параметри:

- field – Поле, яке подібне до поля циклу (типово, вбудований стовпчик циклу)

- current_indices – Якщо має значення True, повертає прив’язку book_id до поточного значення series_index.

get_notes_resource(resource_hash) → dict | None[source]

Return a dict containing the resource data and name or None if no resource with the specified hash is found

get_proxy_metadata(book_id)[source]

Подібний до get_metadata(), але повертає об’єкт ProxyMetadata, який лише читає значення з бази даних на вимогу. Так виходить набагато швидше за get_metadata, якщо потрібен доступ лише до невеликої кількості полів із повернутого об’єкта метаданих.

get_usage_count_by_id(field)[source]

Повертає прив’язку ідентифікатора до кількості використань для усіх значень вказаного поля, яке має бути полем багато-одне або багато-багато.

has_book(mi)[source]

Повертає True тоді і лише тоді, коли база даних містить запис із таким самим заголовком, що і у переданому об’єкті Metadata. При порівнянні не враховується регістр символів. Див. також data_for_has_book().

has_format(book_id, fmt)[source]

Повертає True, лише якщо формат існує на диску

has_id(book_id)[source]

Повертає True, лише якщо вказане значення ід_книги існує у базі даних

import_note(field, item_id, path_to_html_file, path_is_data=False)[source]

Import a previously exported note or an arbitrary HTML file as the note for the specified item

init()[source]

Ініціалізувати цей кеш даними від модуля обробки.

items_with_notes_in_book(book_id: int) → dict[str, dict[int, str]][source]

Return a dict of field to items that have associated notes for that field for the specified book

link_for(field, item_id)[source]

Return the link, if any, for the specified item or None if no link is found

list_extra_files(book_id, use_cache=False, pattern='') → Tuple[ExtraFile, ...][source]

Отримати відомості щодо додаткових файлів у каталозі книги.

Параметри:

- book_id – ідентифікатор книги у базі даних

- pattern – взірець назв файлів, які слід шукати. Порожній взірець означає «усі додаткові файли». У взірцях слід використовувати роздільник «/». Скористайтеся сталою DATA_FILE_PATTERN для встановлення відповідності файлів у каталозі даних.

Повертає:

Кортеж усіх додаткових файлів, що відповідають вказаному взірцю. Кожен елемент кортежу з записом ExtraFile(відн_шлях, шлях_до_файла, статистика), де «відн_шлях» є відносним шляхом до каталогу книги із роздільником /. «статистика» є результатом виклику os.stat() для файла.

merge_extra_files(dest_id, src_ids, replace=False)[source]

Об’єднати додаткові файли з src_ids до dest_id. Файли з конфліктом назв буде автоматично перейменовано, якщо не вказано replace=True. Якщо ж вказано цей параметр, файли буде замінено.

move_book_from_trash(book_id)[source]

Відновити книгу із каталогу смітника

move_format_from_trash(book_id, fmt)[source]

Відновити формат із каталогу смітника

multisort(fields, ids_to_sort=None, virtual_fields=None)[source]

Повертає список упорядкованих ідентифікаторів книг. Якщо ids_to_sort має значення None, буде повернуто ідентифікатори усіх книг.

fields має бути списком двоелементних кортежів у формі (назва_поля, за_зростанням=True або False). Найбільше значення має перша двоелементна комбінація.

notes_data_for(field, item_id) → str[source]

Return all notes data as a dict or None if note does not exist

notes_for(field, item_id) → str[source]

Return the notes document or an empty string if not found

notes_resources_used_by(field, item_id)[source]

Return the set of resource hashes of all resources used by the note for the specified item

pref(name, default=None, namespace=None)[source]

Повертає значення вказаного налаштування або значення, вказане як default, якщо налаштування не встановлено.

read_backup(book_id)[source]

Повертає резервну копію метаданих OPF для книги у форматі рядка байтів або None, якщо така резервна копія вже існує.

remove_books(book_ids, permanent=False)[source]

Вилучити книги, вказані за допомогою параметра book_ids з бази даних і вилучити відповідні файли форматів. Якщо значенням permanent є False, файли форматів надсилатимуться до окремої теки смітника бібліотеки.

remove_formats(formats_map, db_only=False)[source]

Вилучити вказані формат із запису вказаних книг.

Параметри:

- formats_map – Прив’язка book_id до списку форматів книги, які слід вилучити.

- db_only – Якщо має значення True, вилучити запис щодо формату лише з бази даних, не вилучати файл формату з файлової системи.

Повертає:

Карта ідентифікатора книги для встановлення форматів, які буде вилучено з файлової системи цієї книги

remove_items(field, item_ids, restrict_to_book_ids=None)[source]

Вилучити усі записи у вказаному полі з вказаними ідентифікаторами. Повертає список оброблених ідентифікаторів книг. restrict_to_book_ids — необов’язковий набір ідентифікаторів книг. Якщо вказано такий набір, записи вилучатимуться лише із книг цього набору.

rename_extra_files(book_id, map_of_relpath_to_new_relpath, replace=False)[source]

Перейменування додаткових файлів даних

rename_items(field, item_id_to_new_name_map, change_index=True, restrict_to_book_ids=None)[source]

Перейменувати записи з поля «багато-один» або «багато-багато», зокрема з полів міток або циклів.

Параметри:

- change_index – Під час перейменування у полі, подібному до поля циклу, змінити значення series_index.

- restrict_to_book_ids – Необов’язковий набір ідентифікаторів книг, для яких слід виконати перейменовування. Типово, перейменовування виконується для усіх книг.

restore_book(book_id, mi, last_modified, path, formats, annotations=())[source]

Відновити запис книги у базі даних для книги, яка вже існує у файловій системі.

restore_original_format(book_id, original_fmt)[source]

Відновити вказаний формат на основі раніше збереженого ORIGINAL_FORMAT, якщо такий існує. Повертає True, якщо дію виконано успішно. Після успішного відновлення ORIGINAL_FORMAT буде вилучено.

property safe_read_lock

Безпечною дією з блокування на читання є блокування, яке забороняє дії, якщо якщо потік вже має блокування на запис, інакше отримує блокування на читання. Це необхідно для запобігання DowngradeLockErrors, помилкам, які можуть трапитися під час оновлення кешу пошуку, коли існують складені стовпчики. Оновлення кешу пошуку отримує виключне блокування, але пошук складеного стовпчика включає читання значень полів за допомогою ProxyMetadata, який намагається отримати блокування зі спільним доступом. Можливі і інші сценарії, які призводять до цієї ситуації.

Ця властивість повертає новий об’єкт блокування під час кожного доступу. Цей об’єкт блокування не є рекурсивним (з міркувань швидкодії). Його слід використовувати лише у a з інструкцією with cache.safe_read_lock:, інакше можливі небажані наслідки.

save_original_format(book_id, fmt)[source]

Зберегти копію вказаного формату ORIGINAL_FORMAT, переписати усі наявні записи ORIGINAL_FORMAT.

search(query, restriction='', virtual_fields=None, book_ids=None)[source]

Виконати пошук у базі даних за вказаним запитом. Повертає набір ідентифікаторів відповідних до критерію пошуку книг.

Параметри:

- restriction – Обмеження яке буде об’єднано логічним «І» із вказаним запитом. Зауважте, що обмеження кешуються, тому пошук «a І b» буде повільнішим за пошук «a» з обмеженням «b».

- virtual_fields – Для внутрішнього використання (віртуальні поля, зокрема on_device, для пошуку).

- book_ids – Якщо має значення, відмінне від None, пошук виконуватиметься для набору вказаних за допомогою ідентифікаторів книг, а не для усіх книг.

search_notes(fts_engine_query='', use_stemming=True, highlight_start=None, highlight_end=None, snippet_size=None, restrict_to_fields=(), return_text=True, result_type=<class 'tuple'>, process_each_result=None, limit=None)[source]

Search the text of notes using an FTS index. If the query is empty return all notes.

set_conversion_options(options, fmt='PIPE')[source]

параметри мають бути картою у формі {ід_книги:параметри_перетворення}

set_cover(book_id_data_map)[source]

Встановити зображення обкладинки для цієї книги. Значенням data може бути QImage, QPixmap, об’єкт файла або рядок байтів. Також можливе значення None, яке означає, що слід вилучити будь-яке наявне зображення обкладинки.

set_field(name, book_id_to_val_map, allow_case_change=True, do_path_update=True)[source]

Встановлює значення для поля, вказаного за допомогою name. Повертає набір усіх ідентифікаторів книг, на які впливає зміна.

Параметри:

- book_id_to_val_map – Прив’язка book_ids до значень, які слід застосувати.

- allow_case_change – Якщо має значення True, регістр символів у полях багато-одне або багато-багато буде змінено. Наприклад, якщо у книги була мітка мітка1 і ви встановили для іншої книги мітку Мітка1, обидві книги отримають мітку Мітка1, якщо allow_case_change має значення True, і мітку мітка1 у іншому випадку.

- do_path_update – Використовується для внутрішніх потреб програми. Вам не варто змінювати цей параметр.

set_link_map(field, value_to_link_map, only_set_if_no_existing_link=False)[source]

Встановлює посилання для значень пунктів у полі Зауваження: цей метод не змінює значень поза карта_прив“язки_значення_ до_посилання

Параметри:

- field – назва фільтра

- value_to_link_map – dict(значення_поля:посилання, …). Зауважте, що це значення, а не ідентифікатори полів.

Повертає:

книги, які було змінено встановленням посилання

set_metadata(book_id, mi, ignore_errors=False, force_changes=False, set_title=True, set_authors=True, allow_case_change=False)[source]

Встановлює метадані для книги з ідентифікатором id на основі об’єкта Metadata mi

Встановлення значення force_changes=True наказує set_metadata оновити поля у примусовому порядку, навіть якщо mi містить порожні значення. У цьому випадку None відрізняється від empty. Якщо mi.XXX має значення None, XXX не буде замінено, а якщо ні, буде. Мітки, ідентифікатори і атрибути зображення є особливими випадками. Для міток та ідентифікаторів не можна встановлювати значення None, отже їх буде завжди замінено, якщо force_changes має значення True. Вам слід забезпечити наявність у mi значень, які повинна мати книга. Зображення обкладинок замінюються завжди, якщо надано нове зображення обкладинки, але ніколи не вилучаються. Також зауважте, що force_changes не впливає на встановлення назви книги або авторів.

set_notes_for(field, item_id, doc: str, searchable_text: str = '', resource_hashes=(), remove_unused_resources=False) → int[source]

Set the notes document. If the searchable text is different from the document, specify it as searchable_text. If the document references resources their hashes must be present in resource_hashes. Set remove_unused_resources to True to cleanup unused resources, note that updating a note automatically cleans up resources pertaining to that note anyway.

set_pref(name, val, namespace=None)[source]

Встановити вказаний рівень пріоритетності. Див. також pref().

split_if_is_multiple_composite(f, val)[source]

Якщо f є ключем пошуку складеного стовпчика і стовпчик є is_multiple, поділити v на неповторювані непорожні значення. Порівняння виконуватиметься із врахуванням регістру символів. Порядок не зберігатиметься. Повертає list() для сумісності з отримувачами полів метаданих проксі, наприклад мітками.

tags_older_than(tag, delta=None, must_have_tag=None, must_have_authors=None)[source]

Повертає ідентифікатори усіх книг, які мають мітку tag і які є старішими за вказаний час. При порівнянні міток регістр символів не враховується.

Параметри:

- delta – Об’єкт timedelta або None. Якщо має значення None, буде повернуто усі ідентифікатори з вказаною міткою.

- must_have_tag – Якщо встановлено значення None, список відповідників буде обмежено до книг, які мають вказану мітку.

- must_have_authors – Список авторів. Якщо не None, список відповідників буде обмежено до книг, авторами яких є вказані письменники (без врахування регістру символів).

unretire_note_for(field, item_id) → int[source]

Unretire a previously retired note for the specified item. Notes are retired when an item is removed from the database

user_categories_for_books(book_ids, proxy_metadata_map=None)[source]

Повертає категорії користувача для вказаних книг. Параметр proxy_metadata_map є необов’язковим. Він корисний для пришвидшення обробки у контекстах, коли для книг вже існує об’єкт ProxyMetadata. Має повернути прив’язку book_ids до відповідних об’єктів ProxyMetadata.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »

- Налаштовування середовища для розробки calibre »

- Документація з програмного інтерфейсу засобів редагування електронних книг

Документація з програмного інтерфейсу засобів редагування електронних книг

Засоби редагування електронних книг складаються з об’єкта calibre.ebooks.oeb.polish.container.Container, які представляють книгу як збірку HTML + файли ресурсів, і різноманітних засобів, якими можна скористатися для виконання операцій над контейнером. Усі засоби представляються у формі функцій рівня модуля у різноманітних модулях calibre.ebooks.oeb.polish.*.

Ви отримуєте об’єкт-контейнер книги зі шляхом, подібним до такого:

from calibre.ebooks.oeb.polish.container import get_container
container = get_container('Path to book file', tweak_mode=True)

Якщо ви створюєте додаток до редактора електронних книг, ви отримуєте контейнер для книги, яка редагується, подібний до такого:

from calibre.gui2.tweak_book import current_container
container = current_container()
if container is None:
report_error # No book has been opened yet

Об’єкт контейнера
class calibre.ebooks.oeb.polish.container.Container(rootpath, opfpath, log, clone_data=None)[source]

Контейнер складається з відкритої електронної книги, як теки з файлами, і одного файла OPF. Передбачено два важливих поняття:

- Коренева тека. Це основа електронної книги. Усі файли електронної книги зберігаються у цій теці і її підтеках.

- Назви: це шляхи до файлів книги відносно кореневої теки. У них завжди містяться роздільники POSIX і не міститься лапок. Їх можна вважати канонічними ідентифікаторами файлів у книзі. Більшість методів для об’єктів-контейнерів працюють з назвами. Назви завжди записуються у нормальній формі NFC Unicode.

- Клони: для об’єкти-контейнерів передбачено підтримку ефективного клонування на диску. Таке клонування використовується для реалізації контрольних точок у редакторі електронних книг. Для того, щоб це спрацювало, вам ніколи не слід отримувати доступ до файлів у файловій системі безпосередньо. Замість цього, використовуйте raw_data() або open() для читання або запису файлів-компонентів електронної книги.

Для перетворення між гіперпосиланнями і назвами використовуйте методи з цього класу, усі гіперпосилання у цих методах вважаються вказаними у лапках.

abspath_to_name(fullpath, root=None)[source]

Перетворити абсолютний шлях на канонічну назву відносно root

Параметри:

root – Базова тека. Типово, коренева тека об’єкта-контейнера.

add_file(name, data, media_type=None, spine_index=None, modify_name_if_needed=False, process_manifest_item=None)[source]

Додати файл до цього контейнера. Записи для файла автоматично створюються у маніфесті OPF і вмісті (якщо файл є текстовим документом)

add_name_to_manifest(name, process_manifest_item=None)[source]

Додати запис щодо файла з вказаною назвою до маніфесту. Повертає ідентифікатор маніфесту.

add_properties(name, *properties)[source]

Додати вказані властивості до запису маніфесту, вказаного за назвою.

apply_unique_properties(name, *properties)[source]

Переконайтеся, що вказані властивості встановлено лише для запису маніфесту, ідентифікованого за назвою. Ви можете передати None як назву для вилучення властивості з усіх записів.

book_type = 'oeb'

Тип книги (epub для файлів EPUB і azw3 для файлів AZW3)

commit(outpath=None, keep_parsed=False)[source]

Зберегти усі змінені оброблені об’єкти до файлової системи і записати файл електронної книги до outpath.

Параметри:

- output – Каталог, до якого слід записати збережений файл електронної книги. Якщо має значення None, буде використано каталог, у якому зберігалася початкова книга.

- keep_parsed – Якщо має значення True, оброблене представлення збережених записів зберігатиметься у кеші.

commit_item(name, keep_parsed=False)[source]

Зберегти оброблений об’єкт на диск (його буде перетворено на послідовну форму і записано до підлеглого файла). Якщо keep_parsed має значення True, оброблене представлення буде збережено у кеші. Див. також parsed()

dirty(name)[source]

Позначити оброблюваний об’єкт, що відповідає назві, як недооброблений. Див. також: parsed().

exists(name)[source]

Має значення True тоді і лише тоді, коли файл або тека, що відповідає канонічній назві, існує. Зауважте, що дію цієї функції обмежено можливостями файлової системи відповідної операційної системи, зокрема тим, чи розрізняються у цій файловій системі назви за регістром символів. Отже, якщо у файловій системі не розрізняється регістр символів, функція поверне True, навіть якщо назва відрізняється за регістром символів від назви файла у файловій системі. Див. також has_name()

filesize(name)[source]

Повернути розмір у байтах для файла, вказаного канонічною назвою. Автоматично обробляє позначені під час обробки необроблені об’єкти. Див. також: parsed()

generate_item(name, id_prefix=None, media_type=None, unique_href=True)[source]

Додати запис до маніфесту із гіперпосиланням, яке отримано з вказаної назви. Автоматично забезпечує унікальність гіперпосилання і ідентифікатора. Повертає створений запис.

get_file_path_for_processing(name, allow_modification=True)[source]

Подібний до open(), але повертає шлях до файла, а не відкритий об’єкт файла.

property guide_type_map

Прив’язка напрямного типу до канонічної назви

has_name(name)[source]

Повертає True, якщо і лише якщо існує файл із такою самою назвою, яка вказана. На відміну від exists(), цей метод завжди враховує регістр символів.

href_to_name(href, base=None)[source]

Перетворює гіперпосилання (відносно base) на назву. base має бути назвою або None (у цьому випадку буде використано self.root).

insert_into_xml(parent, item, index=None)[source]

Вставити запис до батьківського запису (або дописати запис, якщо index дорівнює None), виправити відступи. Працює лише із самозакривними записами.

is_dir = False

Якщо цей контейнер відповідає розпакованій книзі (каталогу)

iterlinks(name, get_line_numbers=True)[source]

Ітеративно обробити усі посилання у name. Якщо get_line_numbers має значення True, видає результати у формі (посилання, номер_рядка, відступ), де номер_рядка є номером рядка, у якому виявлено посилання, а відступ є кількістю символів від початку цього рядка до посилання. Зауважте, що відступ може включати декілька порожніх рядків, якщо є ненульовим.

make_name_unique(name)[source]

Забезпечує те, що запис name ще не існує у цій книзі. Якщо такий запис існує, повертає змінену версію запису, якої ще не існує.

manifest_has_name(name)[source]

Повертає True, якщо у маніфесті є запис, що відповідає назві

property manifest_id_map

Прив’язка ідентифікаторів маніфесту до канонічних назв

manifest_items_of_type(predicate)[source]

Назви усіх записів маніфесту, чий тип даних відповідає predicate. Значенням predicate може бути множина, список, рядок або функція, яка приймає один аргумент — назву типу даних.

manifest_items_with_property(property_name)[source]

Усі записи маніфесту, які мають вказану властивість

property manifest_type_map

Прив’язка media-type маніфесту до списку канонічних назв цього media-type

property mi

Метадані цієї книги у формі об’єкта Metadata. Зауважте, що цей об’єкт будується «на льоту» кожного разу, коли надсилається запит щодо цієї властивості, тому не варто зловживати її використанням.

name_to_abspath(name)[source]

Перетворити канонічну назву на абсолютний шлях у форматі операційної системи

name_to_href(name, base=None)[source]

Перетворює назву на гіперпосилання відносно base, яким має бути назвою або None (якщо основною є self.root)

property names_that_must_not_be_changed

Набір назв, які заборонено перейменовувати. Залежить від формату файла електронної книги.

property names_that_must_not_be_removed

Набір назв, які заборонено вилучати з контейнера. Залежить від формату файла електронної книги.

property names_that_need_not_be_manifested

Набір назв, які можна не згадувати у маніфесті. Залежить від формату файла електронної книги.

open(name, mode='rb')[source]

Відкрити файл, вказаний за назвою для безпосереднього читання або запису. Зауважте, що у процесі виконання файл буде записано, якщо його було змінено, і вилучено із кешу обробки. Слід завершувати роботу з файлом, перш ніж отримувати доступ до його обробленої версії знову, інакше можуть статися прикрі помилки.

property opf

Оброблений файл OPF

opf_get_or_create(name)[source]

Зручний метод для або повернення першого елемента XML із вказаною назвою, або створення його у елементі opf:package з наступним його поверненням, якщо елемента ще не існувало.

property opf_version

Версія, встановлена на елементі <package> OPF

property opf_version_parsed

Версія, встановлена на елементі <package> OPF, у форматі кортежу цілих чисел

opf_xpath(expr)[source]

Зручний метод для застосування виразу XPath до файла OPF, містить попередньо визначення префікси просторів назв «opf:» та «dc:».

parsed(name)[source]

Повертає оброблене представлення файла, який вказано за назвою (name). Для файлів HTML і XML буде повернуто дерево lxml. Для файлів CSS буде повернуто таблицю стилів css_parser. Зауважте, що оброблені об’єкти кешуються для підвищення швидкодії. Якщо ви вносите будь-які зміни до обробленого об’єкта, вам слід викликати dirty(), щоб повідомити контейнеру, що слід оновити кеш. Див. також replace().

raw_data(name, decode=True, normalize_to_nfc=True)[source]

Повертає необроблені дані, що відповідають файлу, вказаному за назвою

Параметри:

- decode – Якщо має значення True і файл належить до текстового типу MIME, декодувати його і повернути об’єкт unicode замість необробленого набору байтів.

- normalize_to_nfc – Якщо True, повернутий об’єкт unicode нормалізується до нормальної форми NFC, як це вимагається форматами файлів EPUB і AZW3.

relpath(path, base=None)[source]

Перетворити абсолютний шлях (із роздільниками операційної системи) на шлях відносно параметра base (типове значення — self.root). Відносний шлях не є назвою. Для отримання назви скористайтеся abspath_to_name().

remove_from_spine(spine_items, remove_if_no_longer_in_spine=True)[source]

Вилучити вказані записи (за канонічними назвами) із вмісту. Якщо remove_if_no_longer_in_spine має значення True, записи також буде вилучено із книги, не лише із її вмісту.

remove_from_xml(item)[source]

Вилучає запис з батьківського запису, виправляє відступи (працює лише із самозакривними записами)

remove_item(name, remove_from_guide=True)[source]

Вилучити запис, вказаний за назвою, з цього контейнера. Вилучає усі посилання на запис у маніфесті OPF, покажчику та вмісті, а також з усіх кешів внутрішнього використання.

rename(current_name, new_name)[source]

Перейменовує файл з current_name на new_name. Автоматично оновлює усі посилання у файлі, якщо змінюється тека, у якій зберігається файл. Зауважте, що посилання у інших файлах, які можуть посилатися на цей, не оновлюються. Так зроблено з міркувань підвищення швидкодії, оскільки оновлення в усіх файлах варто виконувати один раз у пакетному режимі.

replace(name, obj)[source]

Замінити оброблений об’єкт, що відповідає параметру «name», об’єктом «obj», який має бути подібним об’єктом, тобто деревом lxml для HTML/XML або таблицею стилів css_parser для файлів CSS.

replace_links(name, replace_func)[source]

Замінити усі посилання у name за допомогою функції replace_func, яка має приймати адресу і повертати замінену адресу. Вона також повинна мати атрибут replaced, який встановлюється у значення True, якщо було виконано хоч одну заміну. Зручним способом створення таких функцій є використання класів LinkReplacer та LinkRebaser.

serialize_item(name)[source]

Перетворює оброблений об’єкт (ідентифікований за канонічною назвою) на рядок байтів. Див. parsed().

set_spine(spine_items)[source]

Встановити для вмісту значення «spine_items», де «spine_items» є ітерованою множиною у форматі (назва, лінійність). Видасть повідомлення про помилку, якщо однієї або декількох назв не буде у маніфесті.

property spine_items

Ітератор, який дає шлях для кожного запису у вмісті книги. Див. також spine_iter і spine_items.

property spine_iter

Ітератор, який видає (запис, назву, is_linear) для усіх записів у вмісті книги. запис є елементом lxml, назва є канонічною назвою файла, а is_linear має значення True, якщо запис є лінійним. Див. також spine_names та spine_items.

property spine_names

Ітератор, який дає назву і is_linear для кожного запису у вмісті книги. Див. також spine_iter і spine_items.

Керування файлами компонентів у контейнері

calibre.ebooks.oeb.polish.replace.replace_links(container, link_map, frag_map=<function <lambda>>, replace_in_opf=False)[source]

Замінює посилання на файли у контейнері. Виконує ітеративну обробку усіх файлів у контейнері і змінює у них вказані посилання.

Параметри:

- link_map – Прив’язка старої канонічної назви до нової канонічної назви. Приклад: {'images/old.png': 'images/new.png'}

- frag_map – Функція, яка приймає два аргументи, (назва, прив’язка), і повертає нову прив’язку. Корисно, якщо вам потрібно змінити прив’язки у файлах HTML. Типово не виконує ніяких дій.

- replace_in_opf – Якщо False, посилання у файлі OPF не замінюються.

calibre.ebooks.oeb.polish.replace.rename_files(container, file_map)[source]

Перейменовує файли у контейнері, автоматично оновлюючи усі посилання у них.

Параметри:

file_map – Прив’язка старої канонічної назви до нової канонічної назви. Приклад: {'text/chapter1.html': 'chapter1.html'}.

calibre.ebooks.oeb.polish.replace.get_recommended_folders(container, names)[source]

Повертає теки, які рекомендовано використовувати для вказаних назв файлів. Рекомендація засновується на тому, де зберігається більшість файлів такого типу у контейнері. Якщо файлів певного типу немає, рекомендованою буде тека, у якій зберігається файл OPF.

Структурований друк і автовиправлення помилок обробки

calibre.ebooks.oeb.polish.pretty.fix_html(container, raw)[source]

Виправити усі помилки обробки у HTML, який представлено як рядок у параметрі «raw». Виправлення виконується на основі алгоритму обробки HTML5.

calibre.ebooks.oeb.polish.pretty.fix_all_html(container)[source]

Виправити усі помилки обробки в усіх файлах HTML у контейнері «container». Виправлення виконується на основі алгоритму обробки HTML5.

calibre.ebooks.oeb.polish.pretty.pretty_html(container, name, raw)[source]

Структурований друк HTML, представленого як рядок у raw

calibre.ebooks.oeb.polish.pretty.pretty_css(container, name, raw)[source]

Структурований друк CSS, представленого як рядок у raw

calibre.ebooks.oeb.polish.pretty.pretty_xml(container, name, raw)[source]

Структурований друк XMLS, представленого як рядок у «raw». Якщо name є назвою OPF, буде виконано додаткову обробку друкованих даних, специфічну для OPF.

calibre.ebooks.oeb.polish.pretty.pretty_all(container)[source]

Структурований друк усіх файлів HTML/CSS/XML у контейнері

Керування обгортками книг

calibre.ebooks.oeb.polish.jacket.remove_jacket(container)[source]

Вилучити наявну обгортку, якщо така існує. Повертає False, якщо обгортку не знайдено.

calibre.ebooks.oeb.polish.jacket.add_or_replace_jacket(container)[source]

Або створити нову обгортку на основі метаданих книги або замінити наявну обгортку. Повертає True, якщо було замінено наявну обгортку.

Поділ і об’єднання файлів

calibre.ebooks.oeb.polish.split.split(container, name, loc_or_xpath, before=True, totals=None)[source]

Розділити файл, вказаний за допомогою параметра name, на позиції, вказаній за допомогою loc_or_xpath. Під час поділу програма автоматично змінить усі посилання на файли і прив’язки до них у інших файлах.

Параметри:

- loc_or_xpath – Має бути виразом XPath, наприклад //h:div[@id=»split_here»]. Також може бути loc для внутрішнього використання для реалізації поділу на панелі попереднього перегляду.

- before – Якщо True, поділ відбувається до ідентифікованого елемента, якщо ні, після нього.

- totals – Для внутрішнього використання

calibre.ebooks.oeb.polish.split.multisplit(container, name, xpath, before=True)[source]

Поділити вказаний файл у декількох місцях (усіх теґах, які відповідають вказаному виразу XPath). Див. також split(). Під час поділу усі посилання та прив’язки буде перенесено до розділених файлів.

Параметри:

before – Якщо True, поділ відбувається до ідентифікованого елемента, якщо ні, після нього.

calibre.ebooks.oeb.polish.split.merge(container, category, names, master)[source]

Об’єднати вказані файли у єдиний файл, автоматично перенести усі посилання та прив’язки у відповідних файлах. Усі файли мають бути або файлами HTML, або файлами CSS.

Параметри:

- category – Має бути або 'text' для файлів HTML, або 'styles' для файлів CSS

- names – Список файлів, які слід об’єднати

- master – Визначає, який з об’єднаних файлів буде основним, тобто те, який з файлів залишиться після об’єднання.

Керування обкладинками

calibre.ebooks.oeb.polish.cover.set_cover(container, cover_path, report=None, options=None)[source]

Встановити для обкладинки книги зображення, на яке вказує cover_path.

Параметри:

- cover_path – Або абсолютний шлях до файла зображення, або канонічна назва зображення у файлі книги. Якщо використовується файл з книги, вам слід також встановити певні параметри, див. нижче.

- report – Необов’язкова функція для виклику, яка приймає один аргумент. Її буде викликано із даними щодо завдань, які виконуються.

- options – None або словник, який керує тим, як встановлюється зображення обкладинки. Словник може мати такі записи: keep_aspect: True або False (чи слід зберігати співвідношення розмірів зображення обкладинки у EPUB) no_svg: True або False (чи слід використовувати обгортку зображення обкладинки SVG на титульній сторінці EPUB) existing: True або False (чи вказує cover_path на наявне зображення у книзі)

calibre.ebooks.oeb.polish.cover.mark_as_cover(container, name)[source]

Позначити вказане зображення як зображення обкладинки.

calibre.ebooks.oeb.polish.cover.mark_as_titlepage(container, name, move_to_start=True)[source]

Позначити вказаний файл HTML як файл титульної сторінки EPUB.

Параметри:

move_to_start – Якщо True, файл HTML пересувається на початок комплекту

Робота з CSS

calibre.ebooks.oeb.polish.fonts.change_font(container, old_name, new_name=None)[source]

Змінити гарнітуру шрифту з old_name на new_name. Змінює усі записи гарнітури шрифту у таблицях стилів, теґах стилів та атрибутах стилів. Якщо шрифт old_name є вбудованим, його буде вилучено. Ви можете вказати для new_name назву None, щоб вилучити гарнітуру шрифтів, а не замінювати її.

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]

Вилучає усі невикористані у книзі правила CSS. Невикористаним вважається правило CSS, яке не стосується жодного фрагмента даних у книзі.

Параметри:

- report – Необов’язкова функція для виклику, яка приймає один аргумент. Її буде викликано із даними щодо дій, які виконуються.

- remove_unused_classes – Якщо має значення True, атрибути класу у HTML, які не відповідають жодному з правил CSS, також буде вилучено.

- merge_rules – Якщо True, правила із ідентичними селекторами об’єднуються.

- merge_rules_with_identical_properties – Якщо True, правила із тотожними властивостями буде об’єднано.

- remove_unreferenced_sheets – Якщо True, таблиці стилів, які не використано у даних, буде вилучено

calibre.ebooks.oeb.polish.css.filter_css(container, properties, names=())[source]

Вилучити вказані властивості CSS з усіх правил CSS.

Параметри:

- properties – Встановити властивості для вилучення. Приклад: {'font-family', 'color'}.

- names – Файли, з яких слід вилучити властивості. Типово, усі файли HTML і CSS у книзі.

Робота зі змістом

calibre.ebooks.oeb.polish.toc.from_xpaths(container, xpaths, prefer_title=False)[source]

Створює таблицю змісту на основі списку виразів XPath. Кожен вираз у списку відповідає рівню створення записів змісту. Приклад: за списком ['//h:h1', '//h:h2', '//h:h3'] бдуе створено трирівневу таблицю змісту на основі вмісту теґів <h1>, <h2> і <h3>.

calibre.ebooks.oeb.polish.toc.from_links(container)[source]

Створити зміст на основі посилань у книзі.

calibre.ebooks.oeb.polish.toc.from_files(container)[source]

Створити зміст на основі файлів книги.

calibre.ebooks.oeb.polish.toc.create_inline_toc(container, title=None)[source]

Створює вбудовану таблицю змісту (у форматі HTML) на основі наявної таблиці змісту NCX.

Параметри:

title – Заголовок цієї таблиці змісту.

Засіб редагування книг
class calibre.gui2.tweak_book.plugin.Tool[source]

Основа: object

Базовий клас для окремих інструментів у додатку редагування книг. Серед його корисних учасників:

- self.plugin: посилання на об’єкт calibre.customize.Plugin, до якого належить цей інструмент.

- self. boss

- self. gui

Методи, які слід перевизначити у підкласах:

- create_action()

- register_shortcut()

name = None

Встановіть унікальне значення, яке буде використано як ключ

allowed_in_toolbar = True

Якщо має значення True, користувач зможе вибирати місце для цього інструмента на панелі інструментів додатків.

allowed_in_menu = True

Якщо має значення True, користувач зможе вибирати місце для пункту цього інструмента у меню додатків.

toolbar_button_popup_mode = 'delayed'

Режим контекстного меню для меню (якщо таке передбачено) кнопки панелі інструментів. Можливі значення: „delayed“, „instant“, „button“.

property boss

Об’єкт calibre.gui2.tweak_book.boss.Boss. Використовується для керування інтерфейсом користувача.

property gui

Головне вікно інтерфейсу користувача

property current_container

Повертає поточний об’єкт calibre.ebooks.oeb.polish.container.Container, який відповідає книзі, яка редагується.

register_shortcut(qaction, unique_name, default_keys=(), short_text=None, description=None, **extra_data)[source]

Реєструє комбінацію клавіш, яка запускатиме вказану дію qaction. Цю комбінацію клавіш автоматично буде додано до таблиці комбінацій клавіш у розділі «Клавіатурні скорочення» налаштувань редактора із можливістю внесення змін користувачем.

Параметри:

- qaction – Об’єкт QAction, його буде викликано, якщо користувач натисне налаштовану комбінацію клавіш.

- unique_name – Унікальна назва цього клавіатурного скорочення або дії. Буде використано на внутрішньому рівні, назва не повинна збігатися із назвою будь-якої іншої дії у цьому додатку.

- default_keys – Список типових комбінацій клавіш. Якщо не вказано, типових комбінацій клавіш не визначатиметься. Якщо вказані комбінації клавіш конфліктуватимуть із вбудованими комбінаціями клавіш або комбінаціями клавіш, які налаштовано користувачем або іншими додатками, ці комбінації клавіш буде проігноровано. Конфліктні комбінації користувач повинен налаштувати власноруч за допомогою вікна «Налаштування». Приклад: default_keys=('Ctrl+J', 'F9').

- short_text – Необов’язковий короткий опис цієї дії. Якщо не вказано, буде використано текст для QAction.

- description – Необов’язковий розширений опис цієї дії, буде використано як запис для цього клавіатурного скорочення у списку налаштування скорочень.

create_action(for_toolbar=True)[source]

Створює QAction, яку буде додано або на панель інструментів додатків, або до меню додатків, залежно від for_toolbar. Приклад:

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

Дивись також

Метод register_shortcut().

Керування інтерфейсом користувача редактора

Інтерфейс користувача редактора електронних книг керується єдиним загальним об’єктом. Цей об’єкт має багато корисних методів, яким можна скористатися у коді додатка для виконання типових завдань.

class calibre.gui2.tweak_book.boss.Boss(parent, notify=None)[source]

add_savepoint(msg)[source]

Створює контрольну точку відновлення із назвою, яку вказано параметром msg.

apply_container_update_to_gui(mark_as_modified=True)[source]

Оновлює усі компоненти інтерфейсу користувача так, щоб вони відповідали найсвіжішим даним у поточному контейнері книги.

Параметри:

mark_as_modified – Якщо має значення True, книгу буде позначено як змінену, щоб програма попросила користувача зберегти внесені зміни, коли завершуватиме роботу.

close_editor(name)[source]

Закрити вікно редактора, у якому відбувається редагування файла, вказаного параметром name.

commit_all_editors_to_container()[source]

Записати усі зміни, які внесено користувачем до файлів, які відкрито у вікнах редактора, до контейнера. Вам слід викликати цей метод до виконання будь-яких дій над поточним контейнером.

property currently_editing

Повертає назву файла, який зараз редагують, або None, якщо користувач не редагує жодного файла.

edit_file(name, syntax=None, use_template=None)[source]

Відкрити файл, який вказано параметром name, у редакторі.

Параметри:

- syntax – Тип вмісту файла, наприклад 'text/html'. Якщо не вказано, тип буде визначено за суфіксом назви файла.

- use_template – Шаблон яким слід ініціалізувати відкритий редактор

open_book(path=None, edit_file=None, clear_notify_data=True, open_folder=False, search_text=None)[source]

Відкрити електронну книгу за шляхом path для редагування. Покаже повідомлення про помилку, якщо дані книги зберігаються у непідтримуваному форматі або до поточної книги було внесено зміни, які згодом не було збережено.

Параметри:

edit_file – Назва файла у щойно відкритій для редагування книзі. Може також бути списком назв.

rewind_savepoint()[source]

Скасувати попереднє створення контрольної точки відновлення. Корисно, якщо ви створюєте контрольну точку, а потім перериваєте дію без внесення змін до файлів.

save_book()[source]

Зберегти книгу. Зберігання виконується у фоновому режимі.

set_modified()[source]

Позначити книгу як таку, яку було змінено

show_current_diff(allow_revert=True, to_container=None)[source]

Показує зміни у книзі з часу створення останньої контрольної точки книги.

Параметри:

- allow_revert – Якщо має значення True, діалогове вікно різниці міститиме кнопку, яка надаватиме змогу користувачеві скасувати усі внесені зміни.

- to_container – Об’єкт-контейнер для порівняння з поточним контейнером. Якщо має значення None, буде використано контейнер з попередньої контрольної точки.

show_editor(name)[source]

Показати вікно редактора, у якому відбувається редагування файла, вказаного параметром name.

sync_preview_to_editor()[source]

Синхронізувати позицію на панелі попереднього перегляду зі поточною позицією у вікні редактора.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- наступний |
- попередній |
- Старт »
- Керування цифровими правами (DRM)

Керування цифровими правами (DRM)

Керування цифровими правами (DRM) — загальний термін для позначення технологій керування доступом, які може бути використано виробниками обладнання, видавцями, власниками авторських прав та окремими особами з метою запровадження обмежень на використання цифрових даних та пристроїв. Його також, іноді, зневажливо називають «керуванням цифровими обмеженнями» («Digital Restrictions Management»). Цей термін використовують на позначення будь-якої технології, яка заважає використанню (законному чи незаконному) цифрових даних, якщо таке використання є небажаним або не передбаченим надавачем цих цифрових даних. Термін, загалом, не стосується інших форма захисту від копіювання, які може бути пов’язано із внесенням змін до файла або пристрою, зокрема використанням серійних номерів або файлів-ключів. Його також можуть уживати на позначення обмежень, пов’язаних із специфічними екземплярами цифрових робіт або пристроїв. Технології DRM спрямовано на керування використанням цифрових носіїв шляхом запобігання доступу, копіюванню та перетворення даних на інші формати кінцевими користувачами. Див. статтю у Вікіпедії [https://en.wikipedia.org/wiki/Digital_rights_management].

Які обмеження запроваджує DRM для вас особисто?

Коли ви купуєте книгу із DRM, ви не стаєте її справжнім власником — ви просто купуєте права на її використання у той спосіб, який визначає для вас продавець. DRM обмежує можливості дій з електронними книгами, які ви «придбали». Часто покупці, які придбали книги з DRM, не знають про те, наскільки широкими є ці обмеження. Обмеження забороняють вам змінювати формат вашої електронної книги у бажаний для вас спосіб, зокрема вносити стилістичні зміни, наприклад змінювати розміри шрифтів, хоча існує програмне забезпечення яка надає вам змогу вносити такі зміни до книг, які не захищено DRM. Часто покупці дивуються тому, що придбану ними електронну книгу у певному форматі не можна перетворити на книгу у іншому форматі, якщо цю книгу захищено DRM. Отже, якщо у вас є Amazon Kindle, і ви придбали книгу у Barnes and Nobles, вам слід знати, що якщо книгу захищено DRM, ви не зможете читати її на вашому Kindle. Зауважте, що тут ми говоримо про купівлю книг — не викрадення або користування піратською копією, а саме про «купівлю».

Як працює DRM для авторів книг?

Видавці захищених DRM електронних книг проголошують, що метою впровадження DRM є захист авторів, художньої цілісності їхніх творів та запобігання піратству. Втім, DRM НЕ запобігає піратству. Ті, хто хочете розповсюджувати або споживати отримані внаслідок піратства дані можуть це робити і продовжують це робити. Три основні сучасні схеми захисту DRM електронних книг запроваджено Amazon, Adobe та Barnes and Noble, і усі три схеми DRM вже зламано. Усе, на що здатна система DRM, є ускладнити життя законних користувачів. З цього можна зробити висновок, що насправді ця система шкодить авторам, оскільки ті, хто хотів би придбати книгу, просто шукають піратську версію, оскільки не хочуть проблем із захищеною DRM книгою. Ті, хто скористався б піратською версією без захисту DRM, скористаються піратською версією і тоді, коли книгу захищено. Повторимо ще раз, ключовим аргументом є те, що DRM не запобігає піратству. Тому DRM не лише є непотрібним і шкідливим для покупців електронних книг, але і призводить до недоотримання прибутків авторами.

DRM і свобода

Хоча цифровою формою даних можна скористатися для якнайширшого поширення інформації та творчих робіт та підвищення культурного рівня, деякі видавці у цьому не зацікавлені, оскільки хочуть завадити людям у отриманні свободи доступу просто тому, що хочуть зберегти свій вплив у світі, за розвитком якого вони не встигають.

Чому у calibre не передбачено підтримки DRM?

calibre є вільним програмним забезпеченням із відкритим кодом, а DRM за своєю природою є закритим. Якщо б у calibre було передбачено підтримку відкриття і перегляду файлів, які захищено DRM, програмою було б легко скористатися як інструментом для видалення захисту DRM, тобто забороненої сучасними законами процедури. Принципи програмного забезпечення із відкритим кодом і DRM є несумісними. DRM призначено лише для контролю за користувачем, а програмне забезпечення з відкритим кодом надає користувачу свободу. Ці дві речі просто не може бути реалізовано одночасно.

Якою є точка зору розробників calibre на дії надавачів цифрових даних?

Ми без жодного сумніву підтримуємо думку про те, що автори та інші надавачі творів мають отримувати належну винагороду за їхні зусилля, але DRM не є належним інструментом для забезпечення такої винагороди. Ми розробляємо цю базу даних вільних від DRM електронних книг з різних джерел з метою допомогти вам знайти вільні від DRM альтернативи та допомогти незалежним авторам та видавцям вільних від DRM електронних книг розповсюджувати їхні дані. Ми сподіваємося наша база даних буде для вас корисною. Ми вимагаємо від вас не використовувати піратські методи з даними, доступ до яких ми вам надаємо.

Як допомогти у боротьбі з DRM?

Як особа, яка читає і купує електронні книги, ви можете допомогти у боротьбі з DRM. Не купуйте електронні книги з DRM. Існує декілька видавців, які публікують вільні від DRM електронні книги. Пошукайте у них потрібну вам книгу. Якщо вам подобаються книги певних незалежних авторів, які продають вільні від DRM електронні книги, ви можете підтримати авторів пожертвою. Ці пожертви варті того, оскільки електронні книги таких авторів зазвичай бувають дешевшими (можуть бути виключення) за ті, які ви можете придбати у видавців, книги яких захищено DRM, і, ймовірно, ці книги можна читати на усіх пристроях, власниками яких ви можете стати у майбутньому. Це заощадить ваші гроші на можливе майбутнє повторне придбання книги. Не завдавайте шкоди видавцям та авторам вільних від DRM електронних книг піратським використанням їхніх книг. Надавачі цифрових даних заслуговують на винагороду за їхню роботу. Не карайте їх на намагання спростити ваше життя наданням доступу до вільних від DRM електронних книг. За великим рахунком, завдаючи їх збитків, ви шкодите собі. Якщо ви купуєте книги у продавців, які продають захищені DRM книги і книги, які є вільними від DRM, не знаючи, чи захищено книгу DRM, не забудьте лишити коментар або рецензію, у якій проінформуйте майбутніх покупців про те, чи захищено книгу DRM. Багато продавців вважають неважливим чітке інформування своїх покупців про те, чи захищено електронну книгу DRM. Тут <https://www.defectivebydesign.org/guide/ebooks> ви знайдете настанови щодо життя, яке є вільним від DRM.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- попередній |
- Старт »
- Глосарій

Глосарій

RSS

RSS (Really Simple Syndication) — це формат стрічки новин, що використовується для публікації вмісту, який часто оновлюється, наприклад, новини, статті, пости в блогах, і т.п. Цей формат особливо підходить для читання комп’ютерами, а тому є найкращим способом перенесення вмісту з інтернету у електронну книжку. В інтернеті використовується і багато інших форматів стрічок новин і calibre розуміє більшість з них. Зокрема, calibre підтримує формат ATOM, який переважно використовується для блогів.

рецепт

Рецепт — це набір інструкцій, які вчать calibre, як перетворювати джерело новин з інтернету, зокрема журнал чи блог, на електронну книжку. Рецепт — це код мовою Python [https://www.python.org]. Він здатний перетворювати джерела новин довільної складності на книги. На найпростішому рівні, це просто набір змінних, зокрема адрес, які дають calibre достатньо інформації, щоб вийти в інтернет і завантажити новини.

HTML

HTML* (Hyper Text Mark-Up Language — мова розмітки гіпертекстових документів), підмножина стандартної узагальненої мови розмітки (Standard Generalized Markup Language, SGML) для електронної публікації, є спеціальним стандартом, що використовується для World Wide Web.

CSS

CSS (Cascading Style Sheets - Каскадні таблиці стилів) — мова, що використовується для опису сторінок, написаних на HTML (візуальна презентація).

API

API (Application Programming Interface — Прикладний програмний інтерфейс) — це початковий код інтерфейсу, який надає бібліотека для підтримки запитів на служби, які повинні бути виконані за допомогою комп’ютерних програм.

LRF

LRF — формат електронних книжок, які можна читати за допомогою пристроїв SONY.

URL

URL — (Uniform Resource Locator — уніфікований локатор ресурсів) наприклад, http://example.com

regexp

Формальні вирази надають стислі та гнучкі засоби для ідентифікації рядків тексту, зокрема певних символів, слів чи шаблонів символів. Див. покрокові настанови, щоб ознайомитися зі вступом щодо формальних виразів.

© Copyright Kovid Goyal.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Python Module Index

Python Module Index
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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Overview: module code

All modules for which code is available

- 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.customize

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.customize »
- calibre.customize.conversion

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.db.cache

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.devices.interface

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.devices.usbms.cli

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.devices.usbms.device

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.devices.usbms.driver

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.metadata.book.base

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.metadata.sources.base

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.container

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.cover

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.css

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.fonts

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.jacket

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.pretty

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.replace

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.split

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.ebooks.oeb.polish.toc

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.gui2.actions

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.gui2.preferences

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.gui2.tweak_book.boss

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.gui2.tweak_book.plugin

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.utils.formatter_functions

Source code for 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)

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

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

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)

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)

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)

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

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

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

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

class BuiltinFractionalPart(BuiltinFormatterFunction):
name = 'fractional_part'
arg_count = 1 category = 'Arithmetic'
__doc__ = doc = _('fractional_part(x) -- returns the value after the decimal '
'point. For example, fractional_part(3.14) returns 0.14. '
'Throws an exception if x is not a number.')

def evaluate(self, formatter, kwargs, mi, locals, x):
x = float(x if x and x != 'None' else 0)
return str(modf(x)[0])

class BuiltinTemplate(BuiltinFormatterFunction):
name = 'template'
arg_count = 1 category = 'Recursion'

__doc__ = doc = _('template(x) -- evaluates x as a template. The evaluation is done '
'in its own context, meaning that variables are not shared between '
'the caller and the template evaluation. Because the { and } '
'characters are special, you must use [[ for the { character and '
']] for the } character; they are converted automatically. '
'For example, template(\'[[title_sort]]\') will evaluate the '
'template {title_sort} and return its value. Note also that '
'prefixes and suffixes (the `|prefix|suffix` syntax) cannot be '
'used in the argument to this function when using template program mode.')

def evaluate(self, formatter, kwargs, mi, locals, template):
template = template.replace('[[', '{').replace(']]', '}')
return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)

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)

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

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)

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)

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)

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_)]

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

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

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)

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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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)

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)

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

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

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)

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

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

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

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

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'

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

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'

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

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

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)

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)

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

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

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)

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

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

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

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)

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)

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

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

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

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

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

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)

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)

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

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)

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

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

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

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

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

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

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

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

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

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

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

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)

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

class BuiltinBookValues(BuiltinFormatterFunction):
name = 'book_values'
arg_count = 4 category = 'Template database functions'
__doc__ = doc = _('book_values(column, query, sep, use_vl) -- returns a list '
'of the values contained in the column "column", separated '
'by "sep", in the books found by searching for "query". '
'If use_vl is 0 (zero) then virtual libraries are ignored. '
'This function can be used only in the GUI.')

def evaluate(self, formatter, kwargs, mi, locals, column, query, sep, use_vl):
from calibre.db.fields import rendering_composite_name if (not tweaks.get('allow_template_database_functions_in_composites', False) and formatter.global_vars.get(rendering_composite_name, None)):
raise ValueError(_('The book_values() function cannot be used in a composite column'))
db = self.get_database(mi)
if column not in db.field_metadata:
raise ValueError(_("The column {} doesn't exist").format(column))
try:
ids = db.search_getting_ids(query, None, use_virtual_library=use_vl != '0')
s = set()
for id_ in ids:
f = db.new_api.get_proxy_metadata(id_).get(column, None)
if isinstance(f, (tuple, list)):
s.update(f)
elif f is not None:
s.add(str(f))
return sep.join(s)
except Exception as e:
raise ValueError(e)

class BuiltinHasExtraFiles(BuiltinFormatterFunction):
name = 'has_extra_files'
arg_count = -1 category = 'Template database functions'
__doc__ = doc = _("has_extra_files([pattern]) -- returns the count of extra "
"files, otherwise '' (the empty string). "
"If the optional parameter 'pattern' (a regular expression) "
"is supplied then the list is filtered to files that match "
"pattern before the files are counted. The pattern match is "
"case insensitive. "
'This function can be used only in the GUI.')

def evaluate(self, formatter, kwargs, mi, locals, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return len(files) if len(files) > 0 else ''
except Exception as e:
traceback.print_exc()
raise ValueError(e)

class BuiltinExtraFileNames(BuiltinFormatterFunction):
name = 'extra_file_names'
arg_count = -1 category = 'Template database functions'
__doc__ = doc = _("extra_file_names(sep [, pattern]) -- returns a sep-separated "
"list of extra files in the book's '{}/' folder. If the "
"optional parameter 'pattern', a regular expression, is "
"supplied then the list is filtered to files that match pattern. "
"The pattern match is case insensitive. "
'This function can be used only in the GUI.').format(DATA_DIR_NAME)

def evaluate(self, formatter, kwargs, mi, locals, sep, *args):
if len(args) > 1:
raise ValueError(_('Incorrect number of arguments for function {0}').format('has_extra_files'))
pattern = args[0] if len(args) == 1 else None db = self.get_database(mi).new_api try:
files = tuple(f.relpath.partition('/')[-1] for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN))
if pattern:
r = re.compile(pattern, re.IGNORECASE)
files = tuple(filter(r.search, files))
return sep.join(files)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

class BuiltinExtraFileSize(BuiltinFormatterFunction):
name = 'extra_file_size'
arg_count = 1 category = 'Template database functions'
__doc__ = doc = _("extra_file_size(file_name) -- returns the size in bytes of "
"the extra file 'file_name' in the book's '{}/' folder if "
"it exists, otherwise -1."
'This function can be used only in the GUI.').format(DATA_DIR_NAME)

def evaluate(self, formatter, kwargs, mi, locals, file_name):
db = self.get_database(mi).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
return str(f.stat_result.st_size)
return str(-1)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

class BuiltinExtraFileModtime(BuiltinFormatterFunction):
name = 'extra_file_modtime'
arg_count = 2 category = 'Template database functions'
__doc__ = doc = _("extra_file_modtime(file_name, format_string) -- returns the "
"modification time of the extra file 'file_name' in the "
"book's '{}/' folder if it exists, otherwise -1.0. The "
"modtime is formatted according to 'format_string' "
"(see format_date()). If 'format_string' is empty, returns "
"the modtime as the floating point number of seconds since "
"the epoch. The epoch is OS dependent. "
"This function can be used only in the GUI.").format(DATA_DIR_NAME)

def evaluate(self, formatter, kwargs, mi, locals, file_name, format_string):
db = self.get_database(mi).new_api try:
q = posixpath.join(DATA_DIR_NAME, file_name)
for f in db.list_extra_files(mi.id, use_cache=True, pattern=DATA_FILE_PATTERN):
if f.relpath == q:
val = f.stat_result.st_mtime if format_string:
return format_date(datetime.fromtimestamp(val), format_string)
return str(val)
return str(1.0)
except Exception as e:
traceback.print_exc()
raise ValueError(e)

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)

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.
Востаннє оновлено трав. 03, 2024.

Навігація

- модулі
- Старт »
- Module code »
- calibre.web.feeds.news

Source code for 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.
Востаннє оновлено трав. 03, 2024.

Table of Contents

Підручник користувача calibre

Графічний інтерфейс користувача

1. Дії

1. Додати книги
2. Редагувати метадані
3. Перетворити книги
4. Перегляд
5. Надіслати до пристрою
6. Отримати новини
7. Бібліотека
8. Пристрій
9. Зберегти на диск
10. З’єднатись/Роздати
11. Вилучити книжки

2. Налаштування
3. Каталоги
4. Пошук і упорядковування
5. Інтерфейс пошуку
6. Збереження результатів пошуку
7. Повнотекстовий пошук в усіх книгах
8. Віртуальні бібліотеки
9. Тимчасове позначення книг
10. Визначення метаданих з назв файлів
11. Подробиці щодо книги
12. Переглядач міток
13. Таблиця обкладинок
14. Переглядач обкладинок

15. Додавання нотаток щодо авторів, циклів тощо

16. Швидкий перегляд
17. Завдання
18. Клавіатурні скорочення

Додавання вашого улюбленого сайта новин

1. Повністю автоматизоване отримання даних

1. Блог calibre

2. bbc.co.uk

2. Налаштовування процесу отримання даних

1. Використання друкованої версії bbc.co.uk
2. Заміна стилів статті
3. Вирізання і склеювання
4. Наближений до реальності приклад

3. Підказки щодо створення нових рецептів

4. Додаткові матеріали

5. Документація з програмного інтерфейсу

1. Документація з програмного інтерфейсу для роботи з рецептами

1. BasicNewsRecipe

Переглядач електронних книжок

1. Запуск програми для перегляду електронних книг

2. Навігація електронною книгою

1. Закладки
2. Зміст
3. Навігація за місцем
4. Режим посилань

3. Підсвічування тексту
4. Прочитати уголос
5. Пошук тексту

6. Перехід за посиланнями лише за допомогою клавіатури

7. Налаштовування вигляду і поведінки програми під час читання

8. Пошук у словнику
9. Копіювання тексту та малюнків
10. Збільшення масштабу зображень
11. Клавіатурні скорочення
12. Непридатний до перенесення текст

13. Компонування вашої книги для роботи із переглядачем calibre

Перетворення даних електронних книг

1. Вступ

2. Вигляд та поведінка

1. Шрифти
2. Текст
3. Форматування
4. Стиль
5. Перетворити стилі
6. Перетворення HTML

3. Налаштування сторінки
4. Евристична обробка
5. Пошук і заміна

6. Виявлення структури

1. Поділ на глави і сторінки

2. Різне

7. Зміст

8. Використання зображень як заголовків глав під час перетворення вхідних документів HTML

9. Використання атрибутів теґів для текстових записів у таблиці змісту

10. Як встановлюються та зберігаються параметри для перетворення

11. Підказки для окремих форматів

1. Перетворення документів Microsoft Word

1. Застарілі файли .doc

2. Перетворення документів TXT
3. Перетворення документів PDF
4. Збірки коміксів
5. Демонстрація розширеного форматування EPUB
6. Перетворення документів ODT

7. Перетворення на PDF

1. Верхні і нижні колонтитули

2. Придатний до друку список змісту

3. Нетипові поля сторінок для окремих файлів HTML

Редагування електронних книг

Базові принципи роботи

Навігатор файловою системою

1. Перейменування файлів
2. Об’єднання файлів
3. Зміна порядку текстових файлів
4. Позначення зображення обкладинки
5. Вилучення файлів
6. Експортування файлів

7. Додавання нових зображень, шрифтів та інших елементів або створення порожніх файлів

8. Заміна файлів

9. Ефективне пов’язування таблиць стилів з файлами HTML

Пошук і заміна

1. Збережені результати пошуків
2. Режим функцій
3. Пошук з ігноруванням міток HTML

Автоматизовані інструменти

1. Редагування таблиці вмісту
2. Перевірка книги
3. Додавання зображення обкладинки
4. Вбудовуємо використані шрифти
5. Обрізаємо вбудовані шрифти
6. Удосконалення пунктуації
7. Перетворення властивостей CSS
8. Вилучення зайвих правил CSS
9. Виправлення HTML
10. Удосконалення файлів
11. Вставляння вбудованої таблиці змісту
12. Встановлення семантики
13. Фільтрування даних щодо стилів
14. Оновлення внутрішніх частин книги

Контрольні точки

Панель інтерактивного перегляду

1. Поділ файлів HTML на частини

Панель «Інтерактивний CSS»

Інші інструменти

1. Панель «Зміст»

2. Перевірка правопису слів у книзі

1. Додавання нових словників

3. Вставлення спеціальних символів
4. Панель ревізії коду
5. Перевірка зовнішніх посилань
6. Отримання зовнішніх ресурсів
7. Розподіл файлів за теками на основі типів

8. Імпортування файлів у форматах електронних книг, відмінних від EPUB

9. Інструмент «Звіти»

Особливі можливості редактора коду

1. Підсвічування синтаксичних конструкцій
2. Контекстна довідка
3. Автоматичне доповнення
4. Фрагменти

Сервер даних calibre

1. Доступ до сервера даних з інших пристроїв

1. Доступ до сервера з пристроїв у вашій домашній мережі

1. Діагностика з’єднання у домашній мережі

2. Доступ до сервера з довільного комп’ютера у інтернеті

2. Інтерфейс сервера

1. Список книг

2. Засіб для читання книг

3. Підтримка з боку програм для перегляду інтернету

4. Вмикання підтримки автономної роботи

5. Керування обліковими записами лише з командного рядка

6. Інтегрування сервера даних calibre в інші сервери

1. Використання повного віртуального вузла

2. Використання префікса адреси

7. Створення служби для сервера calibre у сучасній системі Linux

Порівняння електронних книг

1. Опис панелі порівняння

2. Запуск засобу порівняння

1. Порівняння двох файлів електронних книг

2. Порівняння ORIGINAL_FMT та FMT

3. Порівняння контрольної точки з поточним станом книги під час редагування

Редагування метаданих електронної книги

1. Редагування метаданих однієї книжки за раз

1. Отримання метаданих
2. Керування форматами книг
3. Усе про обкладинки

2. Одночасне редагування метаданих групи книг

1. Пошук і заміна

2. Пакетне отримання метаданих

3. Додавання файлів даних до книги

Поширені питання та відповіді на них

Перетворення даних електронних книг у інші формати

1. Підтримку перетворення яких форматів передбачено у calibre?

2. Якими є найкращі початкові формати для перетворення?

3. Перетворення файла PDF не дає бажаних наслідків. Що робити?

4. Як перетворити файл, що містить нелатинські символи або теґи для позначення лапок?

5. Що за проблеми зі списком змісту у файлах MOBI?

6. Як перетворити збірку файлів HTML у певному порядку?

7. Чому файл EPUB, який створено calibre, виявився некоректним?

8. Як скористатися деякими додатковим можливостями інструментів перетворення?

Інтеграція з пристроями

1. Підтримку яких пристроїв передбачено у calibre?

2. Як допомогти у реалізації підтримки вашого пристрою у calibre?

3. Чому calibre не може виявити мій пристрій?

4. Нестандартний або незвичайний пристрій. Як з’єднати з ним комп’ютер?

5. Як скористатися calibre для роботи з iPad/iPhone/iPod touch?

1. Користування сервером даних

6. Як скористатися calibre на моєму телефоні або планшеті під керуванням Android чи Kindle Fire?

1. За допомогою кабеля USB

2. За допомогою бездротового зв’язку

7. Чи можна отримувати доступ до книг calibre за допомогою програми для перегляду інтернету на Kindle або іншому пристрої для читання?

8. Чому у мене не виходити надсилати електронну пошту за допомогою calibre?

9. Пристрій для читання книг монтується у Linux у режимі лише читання. Calibre не зможе отримувати до них доступу?

10. Чому у calibre не передбачено підтримки збірок на Kindle або шаф на Nook?

11. Коли я намагаюся скористатися calibre з моїм Kobo Touch, Glo тощо, програма показує повідомлення про помилку. Що робити?

12. Зображення обкладинок книг, які ви надсилаєте на Kindle з’являються на секунду, а потім замінюються на типове зображення обкладинки.

13. Kindle не показує обкладинки для файлів MOBI на PC/Kindle, на Android/iPad тощо

14. Мною перенесено декілька книжок на Kindle за допомогою calibre. Чому вони не з’явилися у бібліотеці пристрою?

Керування бібліотекою

1. Де зберігаються файли книг?

2. Яким чином calibre обробляє і впорядковує імена авторів?

3. Чому calibre не дає зберігати книги із визначеною користувачем структурою тек?

4. Чому у calibre немає стовпчика для мого типу даних?

5. Чи можна так зробити, щоб з’явився стовпчик із даними щодо форматів або ISBN?

6. Як перенести дані calibre з одного комп’ютера на інший?

7. Список книг у calibre порожній!

8. Чому програма показує повідомлення про помилку, якщо бібліотека calibre зберігається на диску у мережі або у сховищі NAS?

Різне

1. Amazon припиняє надсилання файлів MOBI електронною поштою?

2. Як зробити так, щоб calibre отримувала новини з вашого улюбленого сайта новин?

3. Чому програму названо calibre?

4. Чому calibre показує лише частину встановлених шрифтів у macOS?

5. Чому calibre не запускається у Windows?

6. Чому calibre часом повисає або аварійно завершує роботу?

7. Чому засіб для перегляду електронних книг calibre та засоби редагування книг не працюють у Windows?

8. Використання засобу перегляду або виконання перетворень призводить до повідомлення про заборону доступу (permission denied) у Windows. Що робити?

9. Чому calibre не запускається або аварійно завершує роботу у macOS?

10. Після запуску засобу перегляду книг calibre видно лише біле або чорне полотно. Що робити?

11. Мною отримано програму для встановлення. Чому вона не спрацьовує?

12. Антивірусна програма стверджує, що calibre є вірусом або троянською програмою. Щоб робити?

13. Як створити резервну копію даних calibre?

14. Як прочитати придбані книги EPUB у calibre (або що робити з файлами .acsm)?

15. Бачу повідомлення «Доступ заборонено» («Permission Denied»). Що робити?

16. Як зробити, щоб метадані коментаря було показано на пристрої для читання?

17. Як налаштувати calibre на використання проксі-сервера HTTP?

18. Я хочу реалізувати якісь нові можливості у calibre. Як це зробити?

19. Чому у calibre немає системи автоматичного оновлення?

20. Якими є умови ліцензування calibre?

21. Як запустити calibre з флешки?

22. Як скористатися частинами calibre, зокрема засобом отримання новин та сервером даних, на сервері під керуванням Linux?

Навчальні посібники

Додавання вашого улюбленого сайта новин

1. Повністю автоматизоване отримання даних

1. Блог calibre

2. bbc.co.uk

2. Налаштовування процесу отримання даних

1. Використання друкованої версії bbc.co.uk
2. Заміна стилів статті
3. Вирізання і склеювання
4. Наближений до реальності приклад

3. Підказки щодо створення нових рецептів

4. Додаткові матеріали

5. Документація з програмного інтерфейсу

1. Документація з програмного інтерфейсу для роботи з рецептами

Керування підгрупами книжок, наприклад, «жанр»

1. Налаштовування
2. Пошук
3. Обмеження
4. Корисні шаблонні функції

Підручник з XPath

1. Вибір за назвою теґу
2. Вибір за атрибутами
3. Вибір за вмістом теґу
4. Зразок книги
5. Вбудовані функції XPath

Мова шаблонів calibre

1. Базові шаблони

2. Додаткові можливості з форматування

3. Використання шаблонів для визначення нетипових стовпчиків

4. Шаблони і набори метаданих

5. Використання функцій у шаблонах — режим єдиної функції

6. Загальний режим програмування

7. Складніші програми у виразах шаблонів — режим програмування шаблонів.

8. Режим шаблонів Python
9. Збережені шаблони
10. Надання додаткової інформації шаблонам
11. Зауваження щодо відмінностей між режимами

12. Визначені користувачем шаблонні функції Python

13. Спеціальні нотатки щодо шаблонів збереження або надсилання

14. Підказки

15. Довідник з функцій

1. Reference for all built-in template language functions

Все про використання формальних виразів в calibre

1. Спочатку, декілька попереджень і декілька заохочень

2. Де в calibre ви можете використовувати формальні вирази?

3. Що ж таке ці «формальні вирази»?

4. Трохи пояснень

5. Звучить непогано. Що далі?

6. Ого, чудово! Це може бути корисним!

7. Добре, ці спеціальні символи корисні і усе таке, але що, якщо потрібно знайти крапку або знак питання?

8. Отже, якими є найкорисніші набори?

9. Що якщо потрібно знайти якийсь із декількох рядків? Все значно ускладниться?

10. Ви пропустили…

11. На початку була згадка про те, що формальні вирази можна зробити незалежними від регістру символів. Як це зробити?

12. Здається, починаю розуміти для чого усі ці формальні вирази… Але як ними скористатися у calibre?

1. Перетворення
2. Додавання книг
3. Пакетне редагування метаданих

13. Короткий довідник

1. Короткий довідник щодо синтаксису формальних виразів

14. Подяки

Створення власних додатків для розширення функціональних можливостей calibre

1. Анатомія додатка до calibre

2. Додаток із інтерфейсом користувача

1. __init__.py
2. ui.py
3. main.py
4. Отримання ресурсів з файла ZIP додатка

5. Уможливлення налаштовування вашого додатка користувачем

3. Додатки редагування книг

1. main.py

4. Додавання перекладів вашого додатка

5. Програмний інтерфейс для роботи з додатками

6. Діагностування проблем у додатках

7. Інші приклади додатків

8. Оприлюднення ваших додатків для використання іншими користувачами

Формули в електронних книгах

1. Простий файл HTML із формулами

2. Додаткові відомості

Створення каталогів AZW3 • EPUB • MOBI

1. Вибір книг каталогу
2. Включені розділи
3. Префікси
4. Виключені книги
5. Виключені жанри
6. Інші параметри
7. Нетипові обкладинки каталогів
8. Додаткові інформаційні ресурси

Віртуальні бібліотеки

1. Створення віртуальних бібліотек

1. Корисні приклади віртуальних бібліотек

2. Робота з віртуальними бібліотеками

3. Використання у пошуках віртуальних бібліотек

4. Використання додаткових обмежень

Схема адрес calibre://

1. Перемкнутися на вказану бібліотеку

2. Показати вказану книгу у calibre

3. Відкрити вказану книгу у засобі перегляду електронних книг на вказаній позиції

4. Пошук книг

5. Відкрити вікно подробиць щодо книги для певної книги у деякій бібліотеці

6. Open the notes associated with an author/series/etc.

7. Шістнадцяткове кодування параметрів адреси

Налаштування calibre

1. Змінні середовища
2. Коригування
3. Перевизначення піктограм, шаблонів тощо

4. Створення власної теми піктограм для calibre

5. Налаштовування calibre за допомогою додатків

Інтерфейс командного рядка
Документовані команди
calibre

1. [параметри]

calibre-customize

1. [параметри]

calibre-debug

1. [параметри]

calibre-server

1. [параметри]

calibre-smtp

1. [параметри]

calibredb

1. ЗАГАЛЬНІ ПАРАМЕТРИ
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. ВХІДНІ ПАРАМЕТРИ
2. ВИХІДНІ ПАРАМЕТРИ
3. ВИГЛЯД І ПОВЕДІНКА
4. ЕВРИСТИЧНА ОБРОБКА
5. ПОШУК З ЗАМІНОЮ
6. ВИЯВЛЕННЯ СТРУКТУРИ
7. ЗМІСТ
8. МЕТАДАНІ
9. ДІАГНОСТИКА

ebook-edit

1. [параметри]

ebook-meta

1. [параметри]

ebook-polish

1. [параметри]

ebook-viewer

1. [параметри]

fetch-ebook-metadata

1. [параметри]

lrf2lrs

1. [параметри]

lrfviewer

1. [параметри]

lrs2lrf

1. [параметри]

web2disk

1. [параметри]

Недокументовані команди

Налаштовування середовища для розробки calibre

Засади структури програми

1. Структура коду

Отримання коду

1. Надсилання ваших змін для включення до програми

Середовище для розробки у Windows
Середовище розробки у macOS
Середовище розробки у Linux

Використання окремих «звичайного» і «розробницького» встановлень calibre на одному комп’ютері

Підказки щодо діагностики

1. Використання інструкцій print

2. Використання інтерактивного інтерпретатора Python

3. Використання засобу діагностики Python як віддалений засіб діагностики

4. Використання засобу діагностики у вашому улюбленому комплексному середовищі розробки Python

5. Виконання довільних скриптів у середовищі Python calibre

Використання calibre у ваших проектах

1. Зі встановленого бінарного пакунка calibre

2. Зі встановлених початкових кодів у Linux

Документація з програмного інтерфейсу до різноманітних частин calibre

1. Документація з програмного інтерфейсу для роботи з рецептами

1. BasicNewsRecipe

2. Документація з програмного інтерфейсу для роботи з додатками

1. Додаток
2. FileTypePlugin
3. Додати роботи з метаданими
4. Додатки роботи з каталогом
5. Додатки отримання метаданих
6. Додатки перетворення даних
7. Драйвери пристроїв
8. Дії інтерфейсу користувача
9. Додатки налаштувань

3. Документація з програмного інтерфейсу для роботи з базою даних

1. Cache

4. Документація з програмного інтерфейсу засобів редагування електронних книг

1. Об’єкт контейнера

2. Керування файлами компонентів у контейнері

3. Структурований друк і автовиправлення помилок обробки

4. Керування обгортками книг
5. Поділ і об’єднання файлів
6. Керування обкладинками
7. Робота з CSS
8. Робота зі змістом
9. Засіб редагування книг

10. Керування інтерфейсом користувача редактора

Керування цифровими правами (DRM)

1. Які обмеження запроваджує DRM для вас особисто?

2. Як працює DRM для авторів книг?

3. DRM і свобода

4. Чому у calibre не передбачено підтримки DRM?

5. Якою є точка зору розробників calibre на дії надавачів цифрових даних?

6. Як допомогти у боротьбі з DRM?

Глосарій