├── plugin-import-name-EmbedComicMetadata.txt ├── languages ├── __init__.py ├── lang.py ├── en.py ├── es.py └── de.py ├── test-config ├── conversion │ └── page_setup.py ├── dynamic.pickle.json ├── customize.py.json ├── global.py.json ├── plugins │ └── EmbedComicMetadata.json ├── gui.py.json ├── gui.json └── fonts │ └── scanner_cache.json ├── test-library ├── metadata.db ├── .calnotes │ └── notes.db ├── Firstname Lastname │ ├── Data in both (5) │ │ ├── cover.jpg │ │ ├── Data in both - Firstname Lastname.cbz │ │ └── metadata.opf │ ├── Data only in calibre (2) │ │ ├── cover.jpg │ │ ├── Data only in calibre - Firstname Lastname.cbz │ │ └── metadata.opf │ ├── Data only in comment (4) │ │ ├── cover.jpg │ │ ├── Data only in comment - Firstname Lastname.cbz │ │ └── metadata.opf │ └── Data only in comicinfo (6) │ │ ├── cover.jpg │ │ ├── Data only in comicinfo - Firstname Lastname.cbz │ │ └── metadata.opf └── Unbekannt │ └── zip comic (7) │ ├── zip comic - Unbekannt.zip │ └── metadata.opf ├── images └── embed_comic_metadata.png ├── about.txt ├── .devcontainer └── devcontainer.json ├── .gitignore ├── .vscode └── tasks.json ├── __init__.py ├── README.md ├── comicbookinfo.py ├── ui.py ├── main.py ├── config.py ├── ini.py ├── genericmetadata.py └── comicinfoxml.py /plugin-import-name-EmbedComicMetadata.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /languages/__init__.py: -------------------------------------------------------------------------------- 1 | # just to enable the subdirectory 2 | -------------------------------------------------------------------------------- /test-config/conversion/page_setup.py: -------------------------------------------------------------------------------- 1 | json:{ 2 | "output_profile": "generic_eink" 3 | } -------------------------------------------------------------------------------- /test-library/metadata.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/metadata.db -------------------------------------------------------------------------------- /images/embed_comic_metadata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/images/embed_comic_metadata.png -------------------------------------------------------------------------------- /test-library/.calnotes/notes.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/.calnotes/notes.db -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data in both (5)/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data in both (5)/cover.jpg -------------------------------------------------------------------------------- /test-library/Unbekannt/zip comic (7)/zip comic - Unbekannt.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Unbekannt/zip comic (7)/zip comic - Unbekannt.zip -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in calibre (2)/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in calibre (2)/cover.jpg -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comment (4)/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in comment (4)/cover.jpg -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comicinfo (6)/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in comicinfo (6)/cover.jpg -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data in both (5)/Data in both - Firstname Lastname.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data in both (5)/Data in both - Firstname Lastname.cbz -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in calibre (2)/Data only in calibre - Firstname Lastname.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in calibre (2)/Data only in calibre - Firstname Lastname.cbz -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comment (4)/Data only in comment - Firstname Lastname.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in comment (4)/Data only in comment - Firstname Lastname.cbz -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comicinfo (6)/Data only in comicinfo - Firstname Lastname.cbz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dickloraine/EmbedComicMetadata/HEAD/test-library/Firstname Lastname/Data only in comicinfo (6)/Data only in comicinfo - Firstname Lastname.cbz -------------------------------------------------------------------------------- /test-config/dynamic.pickle.json: -------------------------------------------------------------------------------- 1 | { 2 | "sort_history": [ 3 | [ 4 | "timestamp", 5 | false 6 | ], 7 | [ 8 | "timestamp", 9 | false 10 | ] 11 | ], 12 | "welcome_wizard_device": "default", 13 | "welcome_wizard_was_run": true 14 | } -------------------------------------------------------------------------------- /about.txt: -------------------------------------------------------------------------------- 1 | Embed Comic Metadata 2 | =========================== 3 | 4 | This plugin embeds the calibre metadata into cbz comic files. 5 | It supports writing to the zip-comment (Comicbook Lover style metadata) 6 | and writing a ComicInfo.xml file into the cbz (ComicRack style) 7 | 8 | It also can import the metadata from cbz and cbr into calibre. 9 | -------------------------------------------------------------------------------- /test-config/customize.py.json: -------------------------------------------------------------------------------- 1 | { 2 | "disabled_plugins": { 3 | "__class__": "set", 4 | "__value__": [] 5 | }, 6 | "enabled_plugins": { 7 | "__class__": "set", 8 | "__value__": [] 9 | }, 10 | "filetype_mapping": {}, 11 | "plugin_customization": {}, 12 | "plugins": { 13 | "Embed Comic Metadata": "/root/.config/calibre/plugins/Embed Comic Metadata.zip" 14 | } 15 | } -------------------------------------------------------------------------------- /languages/lang.py: -------------------------------------------------------------------------------- 1 | __license__ = 'GPL v3' 2 | __copyright__ = '2015, dloraine' 3 | __docformat__ = 'restructuredtext en' 4 | 5 | from calibre.utils.localization import get_lang 6 | from .en import en 7 | from .es import es 8 | from .de import de 9 | 10 | 11 | lang_dict = { 12 | "en": en, 13 | "es": es, 14 | "de": de 15 | } 16 | 17 | 18 | lang = get_lang() 19 | _L = lang_dict["en"] 20 | if lang != "en" and lang in lang_dict: 21 | _L.update(lang_dict[lang]) 22 | -------------------------------------------------------------------------------- /test-config/global.py.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_formats_to_existing": false, 3 | "case_sensitive": false, 4 | "check_for_dupes_on_ctl": false, 5 | "database_path": "/root/library1.db", 6 | "filename_pattern": "(?P.+) - (?P<author>[^_]+)", 7 | "input_format_order": [ 8 | "EPUB", 9 | "AZW3", 10 | "MOBI", 11 | "LIT", 12 | "PRC", 13 | "FB2", 14 | "HTML", 15 | "HTM", 16 | "XHTM", 17 | "SHTML", 18 | "XHTML", 19 | "ZIP", 20 | "DOCX", 21 | "ODT", 22 | "RTF", 23 | "PDF", 24 | "TXT" 25 | ], 26 | "installation_uuid": "e07ab65e-a8db-4bb3-872a-5cc7ca6788aa", 27 | "isbndb_com_key": "", 28 | "language": "en", 29 | "library_path": "/root/Calibre Library", 30 | "limit_search_columns": false, 31 | "limit_search_columns_to": [ 32 | "title", 33 | "authors", 34 | "tags", 35 | "series", 36 | "publisher" 37 | ], 38 | "manage_device_metadata": "manual", 39 | "mark_new_books": false, 40 | "migrated": false, 41 | "network_timeout": 5, 42 | "new_book_tags": [], 43 | "numeric_collation": false, 44 | "output_format": "epub", 45 | "read_file_metadata": true, 46 | "saved_searches": {}, 47 | "swap_author_names": false, 48 | "use_primary_find_in_search": true, 49 | "user_categories": {}, 50 | "worker_process_priority": "normal" 51 | } -------------------------------------------------------------------------------- /test-config/plugins/EmbedComicMetadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "auto_count_pages": true, 3 | "cbi_embed": true, 4 | "characters_column": "#characters", 5 | "cix_embed": true, 6 | "colorist_column": "#colorist", 7 | "comicvine_column": "#comicvine", 8 | "convert": true, 9 | "convert_archives": true, 10 | "convert_cbr": true, 11 | "convert_reading": true, 12 | "count_column": "#number_issues", 13 | "count_pages": true, 14 | "cover": true, 15 | "cover_artist_column": "#cover_artist", 16 | "delete_cbr": false, 17 | "editor_column": "#editor", 18 | "embed": true, 19 | "embedcbi": false, 20 | "embedcix": false, 21 | "genre_column": "#genre", 22 | "get_image_sizes": true, 23 | "image_size": true, 24 | "image_size_column": "#image_size", 25 | "import_cbi": false, 26 | "import_cix": false, 27 | "import_tags": false, 28 | "inker_column": "#inker", 29 | "letterer_column": "#letterer", 30 | "locations_column": "#locations", 31 | "main_embed": true, 32 | "main_import": false, 33 | "manga_column": "#manga", 34 | "overwrite_calibre_tags": false, 35 | "pages_column": "#pages", 36 | "penciller_column": "#penciller", 37 | "read_both": true, 38 | "read_cbi": true, 39 | "read_cix": true, 40 | "storyarc_column": "#story_arc", 41 | "swap_names": false, 42 | "teams_column": "#genre", 43 | "volume_column": "#volume" 44 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:noble", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | "features": { 10 | "ghcr.io/devcontainers/features/python:1": {} 11 | }, 12 | 13 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 14 | // "forwardPorts": [], 15 | 16 | // Use 'postCreateCommand' to run commands after the container is created. 17 | "postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y xdg-utils libopengl0 libxcb-cursor0 qt6-base-dev && sudo -v && wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sudo sh /dev/stdin", 18 | 19 | // Configure tool-specific properties. 20 | "customizations": { 21 | "vscode": { 22 | "settings": { 23 | "python.analysis.inlayHints.variableTypes": false, 24 | "python.analysis.typeCheckingMode": "off", 25 | "[python]": { 26 | "editor.formatOnSave": false, 27 | "editor.codeActionsOnSave": { 28 | "source.organizeImports": "never" 29 | } 30 | } 31 | }, 32 | "extensions": [] 33 | }}, 34 | 35 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 36 | "remoteUser": "root" 37 | } 38 | -------------------------------------------------------------------------------- /test-config/gui.py.json: -------------------------------------------------------------------------------- 1 | { 2 | "LRF_conversion_defaults": [], 3 | "LRF_ebook_viewer_options": null, 4 | "asked_library_thing_password": false, 5 | "auto_download_cover": false, 6 | "autolaunch_server": false, 7 | "column_map": [ 8 | "title", 9 | "ondevice", 10 | "authors", 11 | "size", 12 | "timestamp", 13 | "rating", 14 | "publisher", 15 | "tags", 16 | "series", 17 | "pubdate" 18 | ], 19 | "confirm_delete": false, 20 | "cover_flow_queue_length": 6, 21 | "default_send_to_device_action": "DeviceAction:main::False:False", 22 | "delete_news_from_library_on_upload": false, 23 | "disable_animations": false, 24 | "disable_tray_notification": false, 25 | "enforce_cpu_limit": true, 26 | "get_social_metadata": true, 27 | "gui_layout": "wide", 28 | "highlight_search_matches": false, 29 | "internally_viewed_formats": [ 30 | "LRF", 31 | "EPUB", 32 | "LIT", 33 | "MOBI", 34 | "PRC", 35 | "POBI", 36 | "AZW", 37 | "AZW3", 38 | "HTML", 39 | "FB2", 40 | "FBZ", 41 | "PDB", 42 | "RB", 43 | "SNB", 44 | "HTMLZ", 45 | "KEPUB" 46 | ], 47 | "jobs_search_history": [], 48 | "lrf_viewer_search_history": [], 49 | "main_search_history": [], 50 | "main_window_geometry": null, 51 | "match_tags_type": "any", 52 | "new_version_notification": true, 53 | "oldest_news": 60, 54 | "overwrite_author_title_metadata": true, 55 | "plugin_search_history": [], 56 | "save_to_disk_template_history": [], 57 | "scheduler_search_history": [], 58 | "search_as_you_type": false, 59 | "send_to_device_template_history": [], 60 | "send_to_storage_card_by_default": false, 61 | "separate_cover_flow": false, 62 | "shortcuts_search_history": [], 63 | "show_avg_rating": true, 64 | "sort_tags_by": "name", 65 | "systray_icon": false, 66 | "tag_browser_hidden_categories": { 67 | "__class__": "set", 68 | "__value__": [] 69 | }, 70 | "tweaks_search_history": [], 71 | "upload_news_to_device": true, 72 | "use_roman_numerals_for_series_number": true, 73 | "viewer_search_history": [], 74 | "viewer_toc_search_history": [], 75 | "worker_limit": 6 76 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | EmbedComicMetadata.zip 141 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "kill-calibre", 8 | "type": "shell", 9 | "command": "pkill -KILL -f vscode-calibre-task && exit 0", 10 | "presentation": { 11 | "reveal": "never" 12 | }, 13 | "options": { 14 | "statusbar": { 15 | "hide": true 16 | } 17 | } 18 | }, 19 | { 20 | "label": "save-library", 21 | "type": "shell", 22 | "command": "rm -rf ./test-library ./test-config && cp -r ~/'Calibre Library' ./test-library && cp -r ~/.config/calibre ./test-config && rm -rf ./test-library/.caltrash ./test-library/metadata_db_prefs_backup.json test-config/plugins/'Embed Comic Metadata.zip'", 23 | "presentation": { 24 | "reveal": "never" 25 | }, 26 | "options": { 27 | "statusbar": { 28 | "hide": true 29 | } 30 | }, 31 | "problemMatcher": [] 32 | }, 33 | { 34 | "label": "reset-library", 35 | "type": "shell", 36 | "command": "rm -rf ~/'Calibre Library' ~/.config/calibre && cp -r ./test-library ~/'Calibre Library' && cp -r ./test-config ~/.config/calibre", 37 | "presentation": { 38 | "reveal": "never" 39 | }, 40 | "options": { 41 | "statusbar": { 42 | "hide": true 43 | } 44 | }, 45 | "problemMatcher": [] 46 | }, 47 | { 48 | "label": "calibre", 49 | "type": "shell", 50 | "command": "exec -a vscode-calibre-task bash -c 'calibre-debug -s; calibre-customize -b .; calibre'", 51 | "problemMatcher": [], 52 | "presentation": { 53 | "reveal": "never" 54 | }, 55 | "runOptions": { 56 | "instanceLimit": 999 57 | }, 58 | "dependsOn": [ 59 | "kill-calibre" 60 | ], 61 | "dependsOrder": "sequence" 62 | }, 63 | { 64 | "label": "calibre-reset", 65 | "type": "shell", 66 | "command": "exec -a vscode-calibre-task bash -c 'calibre-debug -s; calibre-customize -b .; calibre'", 67 | "problemMatcher": [], 68 | "presentation": { 69 | "reveal": "never" 70 | }, 71 | "runOptions": { 72 | "instanceLimit": 999 73 | }, 74 | "dependsOn": [ 75 | "kill-calibre", 76 | "reset-library" 77 | ], 78 | "dependsOrder": "sequence" 79 | }, 80 | { 81 | "label": "build", 82 | "type": "shell", 83 | "command": "rm EmbedComicMetadata.zip && zip EmbedComicMetadata.zip images/*.* languages/*.* *.py *.md *.txt", 84 | "problemMatcher": [], 85 | "presentation": { 86 | "reveal": "never" 87 | }, 88 | "runOptions": { 89 | "instanceLimit": 999 90 | } 91 | } 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import (unicode_literals, division, absolute_import, 2 | print_function) 3 | 4 | __license__ = 'GPL v3' 5 | __copyright__ = '2015, dloraine' 6 | __docformat__ = 'restructuredtext en' 7 | 8 | # The class that all Interface Action plugin wrappers must inherit from 9 | from calibre.customize import InterfaceActionBase 10 | 11 | 12 | class EmbedComicMetadataBase(InterfaceActionBase): 13 | ''' 14 | This class is a simple wrapper that provides information about the actual 15 | plugin class. The actual interface plugin class is called InterfacePlugin 16 | and is defined in the ui.py file, as specified in the actual_plugin field 17 | below. 18 | 19 | The reason for having two classes is that it allows the command line 20 | calibre utilities to run without needing to load the GUI libraries. 21 | ''' 22 | name = 'Embed Comic Metadata' 23 | description = 'Embeds calibre metadata into comic archives and imports \ 24 | metadata from comic archives into calibre.' 25 | supported_platforms = ['windows', 'osx', 'linux'] 26 | author = 'dloraine' 27 | version = (1, 6, 7) 28 | minimum_calibre_version = (3, 1, 1) 29 | 30 | #: This field defines the GUI plugin class that contains all the code 31 | #: that actually does something. Its format is module_path:class_name 32 | #: The specified class must be defined in the specified module. 33 | actual_plugin = 'calibre_plugins.EmbedComicMetadata.ui:EmbedComicMetadata' 34 | 35 | def is_customizable(self): 36 | ''' 37 | This method must return True to enable customization via 38 | Preferences->Plugins 39 | ''' 40 | return True 41 | 42 | def config_widget(self): 43 | ''' 44 | Implement this method and :meth:`save_settings` in your plugin to 45 | use a custom configuration dialog. 46 | 47 | This method, if implemented, must return a QWidget. The widget can have 48 | an optional method validate() that takes no arguments and is called 49 | immediately after the user clicks OK. Changes are applied if and only 50 | if the method returns True. 51 | 52 | If for some reason you cannot perform the configuration at this time, 53 | return a tuple of two strings (message, details), these will be 54 | displayed as a warning dialog to the user and the process will be 55 | aborted. 56 | 57 | The base class implementation of this method raises NotImplementedError 58 | so by default no user configuration is possible. 59 | ''' 60 | # It is important to put this import statement here rather than at the 61 | # top of the module as importing the config class will also cause the 62 | # GUI libraries to be loaded, which we do not want when using calibre 63 | # from the command line 64 | if self.actual_plugin_: 65 | from calibre_plugins.EmbedComicMetadata.config import ConfigWidget 66 | return ConfigWidget(self.actual_plugin_) 67 | 68 | def save_settings(self, config_widget): 69 | ''' 70 | Save the settings specified by the user with config_widget. 71 | 72 | :param config_widget: The widget returned by :meth:`config_widget`. 73 | ''' 74 | config_widget.save_settings() 75 | 76 | # Apply the changes 77 | ac = self.actual_plugin_ 78 | if ac is not None: 79 | ac.apply_settings() 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embed Comic Metadata 2 | 3 | A Calibre Plugin to manage comic metadata in calibre 4 | 5 | ## Special Notes 6 | 7 | Requires calibre version 1.0.0 or later. 8 | 9 | ## Main Features 10 | 11 | - Can embed the metadata to the zip comment or a ComicInfo.xml file inside the archives 12 | - Can read the above metadata formats and import them into calibre 13 | - Can write many additional metadata into custom columns Can automatically convert cbr/zip/rar files to cbz 14 | - Can embed the calibre cover into cbz comics (experimental) 15 | - Can count the number of pages (image files) in a comic 16 | - Can get the size of the images in the comic file 17 | 18 | ## Usage 19 | 20 | ### To embed calibres metadata into the comic archive: 21 | 22 | - Select the comics that should be updated in the library. 23 | - Click the addon EmbedComicMetadata icon in your toolbar 24 | - (You can select a specific action or open the configuration bei clicking on the small arrow on the icon and selecting the desired option) 25 | 26 | ### To import the comic archive metadata into calibre: 27 | 28 | - Select the comics that should be updated in the library. 29 | - Click the small arrow on the addon EmbedComicMetadata icon in your toolbar 30 | - Click on "Import Metadata from the comic archive into calibre" 31 | 32 | ### Custom Columns: 33 | 34 | You can make custom columns in calibre and populate them with metadata imported with the plugin. In the configuration use the dropdown menu for the columns to select what metadata should be written to what custom column. 35 | 36 | The custom columns you make in calibre should be of the following type, depending on the metadata stored in them. 37 | 38 | #### Comma seperated text, like tags, shown in the tag browser with "Contains names" checked: 39 | 40 | Penciller, Inker, Colorist, Letterer, Cover Artist, Editor 41 | 42 | #### Comma seperated text, like tags, shown in the tag browser: 43 | 44 | Characters, Teams, Locations, Genre 45 | 46 | #### Text, column shown in the tag browser: 47 | 48 | Story Arc, Volume, Number of Issues 49 | 50 | #### Text, but with a fixed set of permitted values: 51 | 52 | * Manga 53 | * Values: No,Yes,YesAndRightToLeft 54 | * Default Value: No 55 | 56 | #### Integer: 57 | 58 | Number of Issues, Volume 59 | 60 | #### Float: 61 | 62 | Image Size 63 | 64 | #### Series like: 65 | 66 | Story Arc 67 | 68 | #### Comment: 69 | 70 | Comic Vine Link 71 | 72 | ### Customizing the main menu: 73 | 74 | The menu in the toolbar can be custimized to your liking through the options in the configuration. 75 | 76 | ### Embed Cover: 77 | 78 | Use with care, just inserts the calibre cover as "00000000_cover" into the comic archive (previously inserted calibre covers are overwritten). 79 | 80 | ### Get image size 81 | 82 | Tries to get the size of the first non cover and non landscape (in case there are double pages, after 9 pages, take that value) image in the comic file in megapixels. 83 | 84 | ## Configuration 85 | 86 | - **Write metadata in zip comment**: This format is used by calibre, if you import comic files and by ComicbookLovers (default: on) 87 | - **Write metadata in ComicInfo.xml**: This format is used by ComicRack and some other comic readers (default: on) 88 | - **Auto convert cbr to cbz**: If a comic has only the cbr format, convert it to store the metadata (default: on) 89 | - **Also convert rar and zip to cbz**: Expand the behaviour for cbr to rars and zips (default: off) 90 | - **Auto convert while importing to calibre**: As above, but even when importing metadata into calibre (default: off) 91 | - **Delete cbr after conversion**: Deletes the cbr format after the conversion (default: off) 92 | - **Swap names to "LN, FN" when importing metadata**: Does just what it says (default: off) 93 | - **Auto count pages if importing**: Count pages automatically if importing metadata into calibre (default: off) 94 | - **Get the image size if importing**: Get the image size automatically if importing metadata into calibre (default: off) 95 | - **Main Button Action**: You can set, what action should be performed if the big toolbar button is pressed. Needs a calibre restart (default: Embed metadata) 96 | - **Menu Buttons**: The dropdown menu on the icon in the toolbar can be custimized to your liking through these options 97 | 98 | ## Acknowledgement 99 | 100 | The handling of the comic metadata is done by using code from [ComicTagger](https://code.google.com/p/comictagger/) 101 | -------------------------------------------------------------------------------- /comicbookinfo.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python class to encapsulate the ComicBookInfo data 3 | """ 4 | 5 | """ 6 | Copyright 2012-2014 Anthony Beville 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | import json 21 | from datetime import datetime 22 | from calibre.utils.localization import calibre_langcode_to_name, canonicalize_lang, lang_as_iso639_1 23 | from calibre_plugins.EmbedComicMetadata.genericmetadata import GenericMetadata 24 | 25 | import sys 26 | 27 | if sys.version_info[0] > 2: 28 | unicode = str 29 | 30 | 31 | class ComicBookInfo: 32 | 33 | def metadataFromString(self, string): 34 | 35 | cbi_container = json.loads(unicode(string, 'utf-8')) 36 | 37 | metadata = GenericMetadata() 38 | 39 | cbi = cbi_container['ComicBookInfo/1.0'] 40 | 41 | # helper func 42 | # If item is not in CBI, return None 43 | def xlate(cbi_entry): 44 | if cbi_entry in cbi: 45 | return cbi[cbi_entry] 46 | else: 47 | return None 48 | 49 | metadata.series = xlate('series') 50 | metadata.title = xlate('title') 51 | metadata.issue = xlate('issue') 52 | metadata.publisher = xlate('publisher') 53 | metadata.month = xlate('publicationMonth') 54 | metadata.year = xlate('publicationYear') 55 | metadata.issueCount = xlate('numberOfIssues') 56 | metadata.comments = xlate('comments') 57 | metadata.credits = xlate('credits') 58 | metadata.genre = xlate('genre') 59 | metadata.volume = xlate('volume') 60 | metadata.volumeCount = xlate('numberOfVolumes') 61 | metadata.language = xlate('language') 62 | metadata.country = xlate('country') 63 | metadata.criticalRating = xlate('rating') 64 | metadata.tags = xlate('tags') 65 | 66 | # make sure credits and tags are at least empty lists and not None 67 | if metadata.credits is None: 68 | metadata.credits = [] 69 | if metadata.tags is None: 70 | metadata.tags = [] 71 | 72 | # need to massage the language string to be ISO 73 | # modified to use a calibre function 74 | if metadata.language is not None: 75 | metadata.language = lang_as_iso639_1(metadata.language) 76 | 77 | metadata.isEmpty = False 78 | 79 | return metadata 80 | 81 | def stringFromMetadata(self, metadata): 82 | 83 | cbi_container = self.createJSONDictionary(metadata) 84 | return json.dumps(cbi_container) 85 | 86 | # verify that the string actually contains CBI data in JSON format 87 | def validateString(self, string): 88 | 89 | try: 90 | cbi_container = json.loads(string) 91 | except: 92 | return False 93 | 94 | return ('ComicBookInfo/1.0' in cbi_container) 95 | 96 | def createJSONDictionary(self, metadata): 97 | 98 | # Create the dictionary that we will convert to JSON text 99 | cbi = dict() 100 | cbi_container = {'appID': 'ComicTagger/', 101 | 'lastModified': str(datetime.now()), 102 | 'ComicBookInfo/1.0': cbi} 103 | 104 | # helper func 105 | def assign(cbi_entry, md_entry): 106 | if md_entry is not None: 107 | cbi[cbi_entry] = md_entry 108 | 109 | # helper func 110 | def toInt(s): 111 | i = None 112 | if type(s) in [str, unicode, int]: 113 | try: 114 | i = int(s) 115 | except ValueError: 116 | pass 117 | return i 118 | 119 | assign('series', metadata.series) 120 | assign('title', metadata.title) 121 | assign('issue', metadata.issue) 122 | assign('publisher', metadata.publisher) 123 | assign('publicationMonth', toInt(metadata.month)) 124 | assign('publicationYear', toInt(metadata.year)) 125 | assign('numberOfIssues', toInt(metadata.issueCount)) 126 | assign('comments', metadata.comments) 127 | assign('genre', metadata.genre) 128 | assign('volume', toInt(metadata.volume)) 129 | assign('numberOfVolumes', toInt(metadata.volumeCount)) 130 | assign('language', calibre_langcode_to_name(canonicalize_lang(metadata.language))) 131 | assign('country', metadata.country) 132 | assign('rating', metadata.criticalRating) 133 | assign('credits', metadata.credits) 134 | assign('tags', metadata.tags) 135 | 136 | return cbi_container 137 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 | from __future__ import (unicode_literals, division, absolute_import, 4 | print_function) 5 | 6 | __license__ = 'GPL v3' 7 | __copyright__ = '2015, dloraine' 8 | __docformat__ = 'restructuredtext en' 9 | 10 | try: 11 | from PyQt5.Qt import QMenu, QIcon, QPixmap 12 | except ImportError: 13 | from PyQt4.Qt import QMenu, QIcon, QPixmap 14 | 15 | from functools import partial 16 | 17 | from calibre.gui2.actions import InterfaceAction 18 | from calibre.gui2 import error_dialog 19 | 20 | from calibre_plugins.EmbedComicMetadata.config import prefs 21 | from calibre_plugins.EmbedComicMetadata.languages.lang import _L 22 | from calibre_plugins.EmbedComicMetadata.ini import ( 23 | get_configuration, CONFIG_NAME, CONFIG_DESCRIPTION, CONFIG_TRIGGER_FUNC, 24 | CONFIG_TRIGGER_ARG, CONFIG_MENU) 25 | 26 | 27 | config = get_configuration() 28 | 29 | 30 | class EmbedComicMetadata(InterfaceAction): 31 | 32 | name = 'Embed Comic Metadata' 33 | 34 | # Declare the main action associated with this plugin 35 | if prefs["main_import"]: 36 | action_spec = (_L['Import Comic Metadata'], None, 37 | _L['Imports the metadata from the comic to calibre'], None) 38 | else: 39 | action_spec = (_L['Embed Comic Metadata'], None, 40 | _L['Embeds calibres metadata into the comic'], None) 41 | 42 | def genesis(self): 43 | # menu 44 | self.menu = QMenu(self.gui) 45 | 46 | # Get the icon for this interface action 47 | icon = self.get_icon('images/embed_comic_metadata.png') 48 | 49 | # The qaction is automatically created from the action_spec defined 50 | # above 51 | self.qaction.setMenu(self.menu) 52 | self.qaction.setIcon(icon) 53 | self.qaction.triggered.connect(self.main_menu_triggered) 54 | 55 | # build menu 56 | self.menu.clear() 57 | self.build_menu() 58 | self.toggle_menu_items() 59 | 60 | def build_menu(self): 61 | for item in config[CONFIG_MENU]["UI_Action_Items"]: 62 | if item[CONFIG_NAME] == "seperator": 63 | self.menu.addSeparator() 64 | continue 65 | elif item[CONFIG_TRIGGER_ARG]: 66 | triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self, item[CONFIG_TRIGGER_ARG]) 67 | else: 68 | triggerfunc = partial(item[CONFIG_TRIGGER_FUNC], self) 69 | self.menu_action(item[CONFIG_NAME], item[CONFIG_DESCRIPTION], triggerfunc) 70 | # add configuration entry 71 | self.menu_action("configure", _L["Configure"], 72 | partial(self.interface_action_base_plugin.do_user_config, (self.gui))) 73 | 74 | def toggle_menu_items(self): 75 | for item in config[CONFIG_MENU]["Items"]: 76 | action = getattr(self, item[CONFIG_NAME]) 77 | action.setVisible(prefs[item[CONFIG_NAME]]) 78 | 79 | def main_menu_triggered(self): 80 | from calibre_plugins.EmbedComicMetadata.main import embed_into_comic, import_to_calibre 81 | 82 | i = prefs["main_import"] 83 | # Check the preferences for what should be done 84 | if (i and prefs['read_cbi'] and prefs['read_cix']) or ( 85 | not i and prefs['cbi_embed'] and prefs['cix_embed']): 86 | action = "both" 87 | elif (i and prefs['read_cbi']) or (not i and prefs['cbi_embed']): 88 | action = "cbi" 89 | elif (i and prefs['read_cix']) or (not i and prefs['cix_embed']): 90 | action = "cix" 91 | else: 92 | return error_dialog(self.gui, _L['Cannot update metadata'], 93 | _L['No embed format selected'], show=True) 94 | 95 | if i: 96 | import_to_calibre(self, action) 97 | else: 98 | embed_into_comic(self, action) 99 | 100 | def apply_settings(self): 101 | # In an actual non trivial plugin, you would probably need to 102 | # do something based on the settings in prefs 103 | prefs 104 | 105 | def menu_action(self, name, title, triggerfunc): 106 | action = self.create_menu_action(self.menu, name, title, icon=None, 107 | shortcut=None, description=None, 108 | triggered=triggerfunc, shortcut_name=None) 109 | setattr(self, name, action) 110 | 111 | def get_icon(self, icon_name): 112 | import os 113 | from calibre.utils.config import config_dir 114 | 115 | # Check to see whether the icon exists as a Calibre resource 116 | # This will enable skinning if the user stores icons within a folder like: 117 | # ...\AppData\Roaming\calibre\resources\images\Plugin Name\ 118 | icon_path = os.path.join(config_dir, 'resources', 'images', self.name, 119 | icon_name.replace('images/', '')) 120 | if os.path.exists(icon_path): 121 | pixmap = QPixmap() 122 | pixmap.load(icon_path) 123 | return QIcon(pixmap) 124 | # As we did not find an icon elsewhere, look within our zip resources 125 | return get_icons(icon_name) 126 | -------------------------------------------------------------------------------- /languages/en.py: -------------------------------------------------------------------------------- 1 | __license__ = 'GPL v3' 2 | __copyright__ = '2015, dloraine' 3 | __docformat__ = 'restructuredtext en' 4 | 5 | en = { 6 | # CONFIG MENU 7 | # custom columns (ini.py) 8 | 'Artists Custom Columns:': 'Artists Custom Columns:', 9 | 'Other Custom Columns:': 'Other Custom Columns:', 10 | 'Penciller:': 'Penciller:', 11 | 'Inker:': 'Inker:', 12 | 'Colorist:': 'Colorist:', 13 | 'Letterer:': 'Letterer:', 14 | 'Cover Artist:': 'Cover Artist:', 15 | 'Editor:': 'Editor:', 16 | 'Story Arc:': 'Story Arc:', 17 | 'Characters:': 'Characters:', 18 | 'Teams:': 'Teams:', 19 | 'Locations:': 'Locations:', 20 | 'Volume:': 'Volume:', 21 | 'Genre:': 'Genre:', 22 | 'Number of issues:': 'Number of issues:', 23 | 'Pages:': 'Pages:', 24 | 'Image size:': 'Image size:', 25 | 'Comicvine link:': 'Comicvine link:', 26 | 'Manga:': 'Manga:', 27 | # options (ini.py) 28 | 'Options:': 'Options:', 29 | 'Write metadata in zip comment': 'Write metadata in zip comment', 30 | 'Write metadata in ComicInfo.xml': 'Write metadata in ComicInfo.xml', 31 | 'Import metadata from zip comment': 'Import metadata from zip comment', 32 | 'Import metadata from ComicInfo.xml': 'Import metadata from ComicInfo.xml', 33 | 'Auto convert cbr to cbz': 'Auto convert cbr to cbz', 34 | 'Also convert rar and zip to cbz': 'Also convert rar and zip to cbz', 35 | 'Auto convert while importing to calibre': 'Auto convert while importing to calibre', 36 | 'Delete cbr after conversion': 'Delete cbr after conversion', 37 | 'Swap names to "LN, FN" when importing metadata': 'Swap names to "LN, FN" when importing metadata', 38 | 'Import tags from comic metadata': 'Import tags from comic metadata', 39 | 'If checked, overwrites the tags in calibre.': 'If checked, overwrites the tags in calibre.', 40 | 'Auto count pages if importing': 'Auto count pages if importing', 41 | 'Get the image size if importing': 'Get the image size if importing', 42 | # main_buton (ini.py) 43 | 'Main Button Action (needs a calibre restart):': 'Main Button Action (needs a calibre restart):', 44 | 'Embed metadata': 'Embed metadata', 45 | 'Import metadata': 'Import metadata', 46 | # toolbar_buttons (ini.py) 47 | 'Menu Buttons:': 'Menu Buttons:', 48 | 'Show embed both button': 'Show embed both button', 49 | 'Show embed cbi button': 'Show embed cbi button', 50 | 'Show embed cix button': 'Show embed cix button', 51 | 'Show import both button': 'Show import both button', 52 | 'Show import cix button': 'Show import cix button', 53 | 'Show import cbi button': 'Show import cbi button', 54 | 'Show convert button': 'Show convert button', 55 | 'Show embed cover button (experimental)': 'Show embed cover button (experimental)', 56 | 'Show count pages button': 'Show count pages button', 57 | 'Show get image size button': 'Show get image size button', 58 | 'Show remove metadata button': 'Show remove metadata button', 59 | 60 | # TOOLBAR MENU 61 | # (ini.py) 62 | 'Import Metadata from the comic archive into calibre': 'Import Metadata from the comic archive into calibre', 63 | 'Import Comic Rack Metadata from the comic archive into calibre': 'Import Comic Rack Metadata from the comic archive into calibre', 64 | 'Import Comment Metadata from the comic archive into calibre': 'Import Comment Metadata from the comic archive into calibre', 65 | 'Embed both Comic Metadata types': 'Embed both Comic Metadata types', 66 | 'Only embed Metadata in zip comment': 'Only embed Metadata in zip comment', 67 | 'Only embed Metadata in ComicInfo.xml': 'Only embed Metadata in ComicInfo.xml', 68 | 'Only convert to cbz': 'Only convert to cbz', 69 | 'Embed the calibre cover': 'Embed the calibre cover', 70 | 'Count pages': 'Count pages', 71 | 'Remove metadata': 'Remove metadata', 72 | 'Get image size': 'Get image size', 73 | # main button (ui.py) 74 | 'Import Comic Metadata': 'Import Comic Metadata', 75 | 'Imports the metadata from the comic to calibre': 'Imports the metadata from the comic to calibre', 76 | 'Embed Comic Metadata': 'Embed Comic Metadata', 77 | 'Embeds calibres metadata into the comic': 'Embeds calibres metadata into the comic', 78 | # config button (ui.py) 79 | 'Configure': 'Configure', 80 | 81 | # COMPLETED MESSAGES (main.py) 82 | 'Updated Calibre Metadata': 'Updated Calibre Metadata', 83 | 'Updated calibre metadata for {} book(s)': 'Updated calibre metadata for {} book(s)', 84 | 'The following books had no metadata: {}': 'The following books had no metadata: {}', 85 | 'Updated comics': 'Updated comics', 86 | 'Updated the metadata in the files of {} comics': 'Updated the metadata in the files of {} comics', 87 | 'The following books were not updated: {}': 'The following books were not updated: {}', 88 | 'Converted files': 'Converted files', 89 | 'Converted {} book(s) to cbz': 'Converted {} book(s) to cbz', 90 | 'The following books were not converted: {}': 'The following books were not converted: {}', 91 | 'Updated Covers': 'Updated Covers', 92 | 'Embeded {} covers': 'Embeded {} covers', 93 | 'The following covers were not embeded: {}': 'The following covers were not embeded: {}', 94 | 'Counted pages': 'Counted pages', 95 | 'Counted pages in {} comics': 'Counted pages in {} comics', 96 | 'The following comics were not counted: {}': 'The following comics were not counted: {}', 97 | 'The following comics were converted to cbz: {}': 'The following comics were converted to cbz: {}', 98 | 'Removed metadata': 'Removed metadata', 99 | 'Removed metadata in {} comics': 'Removed metadata in {} comics', 100 | 'The following comics did not have metadata removed: {}': 'The following comics did not have metadata removed: {}', 101 | 102 | # ERRORS 103 | # (ui.py) 104 | 'Cannot update metadata': 'Cannot update metadata', 105 | 'No embed format selected': 'No embed format selected', 106 | # (main.py) 107 | 'Cannot update metadata': 'Cannot update metadata', 108 | 'No books selected': 'No books selected' 109 | # end 110 | } 111 | -------------------------------------------------------------------------------- /languages/es.py: -------------------------------------------------------------------------------- 1 | __license__ = 'GPL v3' 2 | __copyright__ = '2015, dloraine' 3 | __docformat__ = 'restructuredtext es' 4 | 5 | es = { 6 | # CONFIG MENU 7 | # custom columns (ini.py) 8 | 'Artists Custom Columns:': 'Columnas personalizadas de artistas:', 9 | 'Other Custom Columns:': 'Otras columnas personalizadas:', 10 | 'Penciller:': 'Dibujante:', 11 | 'Inker:': 'Entintador:', 12 | 'Colorist:': 'Colorista:', 13 | 'Letterer:': 'Rotulador:', 14 | 'Cover Artist:': 'Artista de portada:', 15 | 'Editor:': 'Editor:', 16 | 'Story Arc:': 'Arco narrativo:', 17 | 'Characters:': 'Caracteres:', 18 | 'Teams:': 'Equipos:', 19 | 'Locations:': 'Ubicaciones:', 20 | 'Volume:': 'Volumen:', 21 | 'Genre:': 'Género:', 22 | 'Number of issues:': 'Número de volumenes:', 23 | 'Pages:': 'Páginas:', 24 | 'Image size:': 'Tamaño Imagen:', 25 | 'Comicvine link:': 'Comicvine enlace:', 26 | 'Manga:': 'Manga:', 27 | # options (ini.py) 28 | "Options:": "Opciones:", 29 | 'Write metadata in zip comment': 'Escribir metadatos en comentario zip', 30 | 'Write metadata in ComicInfo.xml': 'Escribir metadatos en ComicInfo.xml', 31 | 'Import metadata from zip comment': 'Importar metadatos desde comentario zip', 32 | 'Import metadata from ComicInfo.xml': 'Importar metadatos desde Comicinfo.xml', 33 | 'Auto convert cbr to cbz': 'Conversión automática de cbr a cbz', 34 | 'Also convert rar and zip to cbz': 'También convierta rar y zip a cbz', 35 | 'Auto convert while importing to calibre': 'Conversión automática al importar a calibre', 36 | 'Delete cbr after conversion': 'Eliminar cbr después de la conversión', 37 | 'Swap names to "LN, FN" when importing metadata': 'Cambie los nombres a "LN, FN" al importar metadatos', 38 | 'Import tags from comic metadata': 'Importar etiquetas de metadatos de cómics', 39 | 'If checked, overwrites the tags in calibre.': 'Si está marcado, sobrescribe las etiquetas en calibre.', 40 | 'Auto count pages if importing': 'Conteo automático de páginas si se importa', 41 | 'Get the image size if importing': 'Obtenga el tamaño de la imagen si la importa', 42 | # main_buton (ini.py) 43 | "Main Button Action (needs a calibre restart):": "Acción del botón principal (necesita reiniciar calibre):", 44 | 'Embed metadata': 'Insertar metadatos', 45 | 'Import metadata': 'Importar metadatatos', 46 | # toolbar_buttons (ini.py) 47 | "Menu Buttons:": "Botones de menú:", 48 | 'Show embed both button': 'Mostrar botón para insertar ambos', 49 | 'Show embed cbi button': 'Mostrar botón insertar cbi', 50 | 'Show embed cix button': 'Mostrar botón insertar cix', 51 | 'Show import both button': 'Mostrar botón importar ambos', 52 | 'Show import cix button': 'Mostrar botón importar cix', 53 | 'Show import cbi button': 'Mostrar botón importar cbi', 54 | 'Show convert button': 'Mostrar botón de conversión', 55 | 'Show embed cover button (experimental)': 'Mostrar botón para insertar portada (experimental)', 56 | 'Show count pages button': 'Botón Mostrar recuento de páginas', 57 | 'Show get image size button': 'Mostrar botón para obtener tamaño de imagen', 58 | 'Show remove metadata button': 'Mostrar el botón para eliminar metadatos', 59 | 60 | # TOOLBAR MENU 61 | # (ini.py) 62 | 'Import Metadata from the comic archive into calibre': 'Importar metadatos del archivo de cómics a calibre', 63 | "Import Comic Rack Metadata from the comic archive into calibre": "Importar metadatos de Comic Rack del archivo de cómics a calibre", 64 | "Import Comment Metadata from the comic archive into calibre": "Importar metadatos de comentarios del archivo de cómics a calibre", 65 | "Embed both Comic Metadata types": "Incrustar ambos tipos de metadatos de cómic", 66 | "Only embed Metadata in zip comment": "Incrustar sólo metadatos en el comentario zip", 67 | "Only embed Metadata in ComicInfo.xml": "Solo incrustar metadatos en ComicInfo.xml", 68 | "Only convert to cbz": "Solo convertir a cbz", 69 | "Embed the calibre cover": "Incrustar la cubierta del calibre", 70 | "Count pages": "Contar páginas", 71 | 'Remove metadata': 'Eliminar metadatos', 72 | "Get image size": "Obtener tamaño de imagen", 73 | # main button (ui.py) 74 | 'Import Comic Metadata': 'Importar metadatos del cómic', 75 | 'Imports the metadata from the comic to calibre': 'Importa los metadatos del cómic a calibre', 76 | 'Embed Comic Metadata': 'Incrustar metadatos del cómic', 77 | 'Embeds calibres metadata into the comic': 'Incrusta metadatos de calibre en el cómic', 78 | # config button (ui.py) 79 | "Configure": "Configurar", 80 | 81 | # COMPLETED MESSAGES (main.py) 82 | "Updated Calibre Metadata": "Metadatos de calibre actualizados", 83 | 'Updated calibre metadata for {} book(s)': 'Metadatos de calibre actualizados para {} libro(s)', 84 | 'The following books had no metadata: {}': 'Los siguientes libros no tenían metadatos: {}', 85 | "Updated comics": "Cómics actualizados", 86 | 'Updated the metadata in the files of {} comics': 'Actualizados los metadatos en los archivos de {} comics', 87 | 'The following books were not updated: {}': 'Los siguientes libros no fueron actualizados: {}', 88 | "Converted files": "Archivos convertidos", 89 | 'Converted {} book(s) to cbz': 'Convertido {} libro(s) a cbz', 90 | 'The following books were not converted: {}': 'Los siguientes libros no se convirtieron: {}', 91 | "Updated Covers": "Portadas Actualizadas", 92 | 'Embeded {} covers': 'Portadas {} incrustadas', 93 | 'The following covers were not embeded: {}': 'Las siguientes portadas no fueron incrustadas: {}', 94 | "Counted pages": "Páginas contadas", 95 | 'Counted pages in {} comics': 'Páginas contadas en {} cómics', 96 | 'The following comics were not counted: {}': 'No se contaron los siguientes cómics: {}', 97 | "The following comics were converted to cbz: {}": "Los siguientes cómics fueron convertidos a cbz: {}", 98 | 'Removed metadata': 'Metadatos eliminados', 99 | 'Removed metadata in {} comics': 'Se eliminaron metadatos en {} cómics', 100 | 'The following comics did not have metadata removed: {}': 'A los siguientes cómics no se les eliminaron metadatos: {}', 101 | 102 | # ERRORS 103 | # (ui.py) 104 | 'Cannot update metadata': 'No se pueden actualizar los metadatos', 105 | 'No embed format selected': 'No se ha seleccionado ningún formato de inserción', 106 | # (main.py) 107 | 'Cannot update metadata': 'No se pueden actualizar los metadatos', 108 | 'No books selected': 'No hay libros seleccionados' 109 | # end 110 | } 111 | -------------------------------------------------------------------------------- /languages/de.py: -------------------------------------------------------------------------------- 1 | __license__ = 'GPL v3' 2 | __copyright__ = '2015, dloraine' 3 | __docformat__ = 'restructuredtext en' 4 | 5 | de = { 6 | # CONFIG MENU 7 | # custom columns (ini.py) 8 | 'Artists Custom Columns:': 'Eigene Spalten für Künstler:', 9 | 'Other Custom Columns:': 'Andere Eigene Spalten:', 10 | 'Penciller:': 'Zeichner:', 11 | 'Inker:': 'Inker:', 12 | 'Colorist:': 'Colorist:', 13 | 'Letterer:': 'Letterer:', 14 | 'Cover Artist:': 'Cover Künstler:', 15 | 'Editor:': 'Editor:', 16 | 'Story Arc:': 'Story Bogen:', 17 | 'Characters:': 'Charactere:', 18 | 'Teams:': 'Teams:', 19 | 'Locations:': 'Orte:', 20 | 'Volume:': 'Band:', 21 | 'Genre:': 'Genre:', 22 | 'Number of issues:': 'Anzahl der Ausgaben:', 23 | 'Pages:': 'Seiten:', 24 | 'Image size:': 'Bildgröße:', 25 | 'Comicvine link:': 'Comicvine link:', 26 | 'Manga:': 'Manga:', 27 | # options (ini.py) 28 | 'Options:': 'Optionen:', 29 | 'Write metadata in zip comment': 'Schreibe die Metadaten in den Zip-Kommentar', 30 | 'Write metadata in ComicInfo.xml': 'Schreibe die Metadaten in die ComicInfo.xml Datei', 31 | 'Import metadata from zip comment': 'Importiere die Metadaten aus dem Zip-Kommentar', 32 | 'Import metadata from ComicInfo.xml': 'Importiere die Metadaten aus der ComicInfo.xml Datei', 33 | 'Auto convert cbr to cbz': 'Konvertiere cbr Dateien automatisch zu cbz Dateien', 34 | 'Also convert rar and zip to cbz': 'Konvertiere auch rar und zip Dateien zu cbz Dateien', 35 | 'Auto convert while importing to calibre': 'Konvertiere auch beim Importieren automatisch', 36 | 'Delete cbr after conversion': 'Lösche die cbr Datei nach dem Konvertieren', 37 | 'Swap names to "LN, FN" when importing metadata': 'Vertausche Vor- und Nachnamen beim Importieren', 38 | 'Import tags from comic metadata': 'Importiere die Schlagwörter aus den Comic-Metadaten', 39 | 'If checked, overwrites the tags in calibre.': 'Überschreibe die Schlagwörter in Calibre', 40 | 'Auto count pages if importing': 'Zähle Seiten beim importieren', 41 | 'Get the image size if importing': 'Bestimme die Bildgröße beim importieren', 42 | # main_buton (ini.py) 43 | 'Main Button Action (needs a calibre restart):': 'Haupt-Button Aktion (erfordert einen Neustart):', 44 | 'Embed metadata': 'Bette Metadaten ein', 45 | 'Import metadata': 'Importiere Metadaten', 46 | # toolbar_buttons (ini.py) 47 | 'Menu Buttons:': 'Menü Buttons:', 48 | 'Show embed both button': 'Zeige den Button um beide Metadaten einzubetten', 49 | 'Show embed cbi button': 'Zeige den Button um cbi-Metadaten einzubetten', 50 | 'Show embed cix button': 'Zeige den Button um cix-Metadaten einzubetten', 51 | 'Show import both button': 'Zeige den Button um beide Metadaten zu importieren', 52 | 'Show import cix button': 'Zeige den Button um cix-Metadaten zu importieren', 53 | 'Show import cbi button': 'Zeige den Button um cbi-Metadaten zu importieren', 54 | 'Show convert button': 'Zeige den Konvertierungs-Button', 55 | 'Show embed cover button (experimental)': 'Zeige den Button zum einbetten des Covers (experimentell)', 56 | 'Show count pages button': 'Zeige den Button zum zählen der Seiten', 57 | 'Show get image size button': 'Zeige den Button zur Bestimmung der Bildgröße', 58 | 'Show remove metadata button': 'Zeige den Button zur Entfernung von Metadaten', 59 | 60 | # TOOLBAR MENU 61 | # (ini.py) 62 | 'Import Metadata from the comic archive into calibre': 'Importiere die Metadaten aus dem Comic', 63 | 'Import Comic Rack Metadata from the comic archive into calibre': 'Importiere die Comic Rack Metadaten aus dem Comic', 64 | 'Import Comment Metadata from the comic archive into calibre': 'Importiere die Metadaten aus dem Kommentar des Comics', 65 | 'Embed both Comic Metadata types': 'Bette beide Metadaten-Typen ein', 66 | 'Only embed Metadata in zip comment': 'Bette die Metadaten nur in den Zip-Kommentar ein', 67 | 'Only embed Metadata in ComicInfo.xml': 'Bette die Metadaten nur in die ComicInfo.xml Datei ein', 68 | 'Only convert to cbz': 'Konvertiere die Dateien zu cbz Dateien', 69 | 'Embed the calibre cover': 'Bette das Calibre Cover ein', 70 | 'Count pages': 'Zähle Seiten', 71 | 'Remove metadata': 'Entferne Metadaten', 72 | 'Get image size': 'Bestimme die Bildgröße', 73 | # main button (ui.py) 74 | 'Import Comic Metadata': 'Metadaten importieren', 75 | 'Imports the metadata from the comic to calibre': 'Importiere die Metadaten aus dem Comic', 76 | 'Embed Comic Metadata': 'Metadaten einbetten', 77 | 'Embeds calibres metadata into the comic': 'Bette Calibres Metadaten in das Comic ein', 78 | # config button (ui.py) 79 | 'Configure': 'Optionen', 80 | 81 | # COMPLETED MESSAGES (main.py) 82 | 'Updated Calibre Metadata': 'Calibre Metadaten wurden aktualisiert', 83 | 'Updated calibre metadata for {} book(s)': 'Die Metadaten für {} Comics wurden aktualisiert', 84 | 'The following books had no metadata: {}': 'Die folgenden Comics haben keine Metadaten: {}', 85 | 'Updated comics': 'Comic Dateien wurden aktualisiert', 86 | 'Updated the metadata in the files of {} comics': 'Die Metadaten in {} Comic Dateien wurden aktualisiert', 87 | 'The following books were not updated: {}': 'Die folgenden Comic Dateien wurden nicht aktualisiert: {}', 88 | 'Converted files': 'Dateien wurden konverttiert', 89 | 'Converted {} book(s) to cbz': '{} Comics wurden in das cbz Format konvertiert', 90 | 'The following books were not converted: {}': 'Die folgenden Comics wurden nicht konvertiert: {}', 91 | 'Updated Covers': 'Die Cover wurden aktualisiert', 92 | 'Embeded {} covers': '{} Cover wurden eingebettet', 93 | 'The following covers were not embeded: {}': 'Die folgenden Cover wurden nicht eingebettet: {}', 94 | 'Counted pages': 'Seiten gezählt', 95 | 'Counted pages in {} comics': 'Seiten in {} comics gezählt', 96 | 'The following comics were not counted: {}': 'Die folgenden Comics wurden nicht gezählt: {}', 97 | 'The following comics were converted to cbz: {}': 'Die folgenden Comics wurden in das cbz Format konvertiert: {}', 98 | 'Removed metadata': 'Metadaten wurden entfernt', 99 | 'Removed metadata in {} comics': 'Metadaten aus {} Comics entfernt', 100 | 'The following comics did not have metadata removed: {}': 'Aus den folgenden Comics wurden die Metadaten nicht entfernt: {}', 101 | 102 | # ERRORS 103 | # (ui.py) 104 | 'Cannot update metadata': 'Metadaten können nicht aktualisiert werden', 105 | 'No embed format selected': 'Kein passendes Format gewählt', 106 | # (main.py) 107 | 'Cannot update metadata': 'Metadaten können nicht aktualisiert werden', 108 | 'No books selected': 'Keine Bücher wurden ausgewählt' 109 | # end 110 | } 111 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import (unicode_literals, division, absolute_import, 2 | print_function) 3 | 4 | __license__ = 'GPL v3' 5 | __copyright__ = '2015, dloraine' 6 | __docformat__ = 'restructuredtext en' 7 | 8 | from functools import partial 9 | from calibre.gui2 import error_dialog, info_dialog 10 | 11 | from calibre_plugins.EmbedComicMetadata.config import prefs 12 | from calibre_plugins.EmbedComicMetadata.languages.lang import _L 13 | from calibre_plugins.EmbedComicMetadata.comicmetadata import ComicMetadata 14 | 15 | import sys 16 | 17 | python3 = sys.version_info[0] > 2 18 | 19 | def import_to_calibre(ia, action): 20 | def _import_to_calibre(metadata): 21 | metadata.get_comic_metadata_from_file() 22 | if action == "both" and metadata.comic_metadata: 23 | metadata.import_comic_metadata_to_calibre(metadata.comic_metadata) 24 | elif action == "cix" and metadata.cix_metadata: 25 | metadata.import_comic_metadata_to_calibre(metadata.cix_metadata) 26 | elif action == "cbi" and metadata.cbi_metadata: 27 | metadata.import_comic_metadata_to_calibre(metadata.cbi_metadata) 28 | else: 29 | return False 30 | return True 31 | 32 | iterate_over_books(ia, _import_to_calibre, 33 | _L["Updated Calibre Metadata"], 34 | _L['Updated calibre metadata for {} book(s)'], 35 | _L['The following books had no metadata: {}'], 36 | prefs['convert_reading']) 37 | 38 | 39 | def embed_into_comic(ia, action): 40 | def _embed_into_comic(metadata): 41 | if metadata.format != "cbz": 42 | return False 43 | metadata.overlay_metadata() 44 | if action == "both" or action == "cix": 45 | metadata.embed_cix_metadata() 46 | if action == "both" or action == "cbi": 47 | metadata.embed_cbi_metadata() 48 | metadata.add_updated_comic_to_calibre() 49 | return True 50 | 51 | iterate_over_books(ia, _embed_into_comic, 52 | _L["Updated comics"], 53 | _L['Updated the metadata in the files of {} comics'], 54 | _L['The following books were not updated: {}']) 55 | 56 | 57 | def convert(ia): 58 | iterate_over_books(ia, partial(convert_to_cbz, ia), 59 | _L["Converted files"], 60 | _L['Converted {} book(s) to cbz'], 61 | _L['The following books were not converted: {}'], 62 | False) 63 | 64 | 65 | def embed_cover(ia): 66 | def _embed_cover(metadata): 67 | if metadata.format != "cbz": 68 | return False 69 | metadata.update_cover() 70 | metadata.add_updated_comic_to_calibre() 71 | return True 72 | 73 | iterate_over_books(ia, _embed_cover, 74 | _L["Updated Covers"], 75 | _L['Embeded {} covers'], 76 | _L['The following covers were not embeded: {}']) 77 | 78 | 79 | def count_pages(ia): 80 | def _count_pages(metadata): 81 | if metadata.format != "cbz": 82 | return False 83 | return metadata.action_count_pages() 84 | 85 | iterate_over_books(ia, _count_pages, 86 | _L["Counted pages"], 87 | _L['Counted pages in {} comics'], 88 | _L['The following comics were not counted: {}']) 89 | 90 | 91 | def remove_metadata(ia): 92 | def _remove_metadata(metadata): 93 | if metadata.format != "cbz": 94 | return False 95 | metadata.remove_embedded_metadata() 96 | metadata.add_updated_comic_to_calibre() 97 | return True 98 | 99 | iterate_over_books(ia, _remove_metadata, 100 | _L["Removed metadata"], 101 | _L['Removed metadata in {} comics'], 102 | _L['The following comics did not have metadata removed: {}']) 103 | 104 | 105 | def get_image_size(ia): 106 | def _get_image_size(metadata): 107 | if metadata.format != "cbz": 108 | return False 109 | return metadata.action_picture_size() 110 | 111 | iterate_over_books(ia, _get_image_size, 112 | _L["Updated Calibre Metadata"], 113 | _L['Updated calibre metadata for {} book(s)'], 114 | _L['The following books were not updated: {}']) 115 | 116 | 117 | def iterate_over_books(ia, func, title, ptext, notptext, 118 | should_convert=None, 119 | convtext=_L["The following comics were converted to cbz: {}"]): 120 | ''' 121 | Iterates over all selected books. For each book, it checks if it should be 122 | converted to cbz and then applies func to the book. 123 | After all books are processed, gives a completion message. 124 | ''' 125 | processed = [] 126 | not_processed = [] 127 | converted = [] 128 | 129 | if should_convert is None: 130 | should_convert = prefs["convert_cbr"] 131 | 132 | # iterate through the books 133 | for book_id in get_selected_books(ia): 134 | metadata = ComicMetadata(book_id, ia) 135 | 136 | # sanity check 137 | if metadata.format is None: 138 | not_processed.append(metadata.info) 139 | continue 140 | 141 | if should_convert and convert_to_cbz(ia, metadata): 142 | converted.append(metadata.info) 143 | 144 | if func(metadata): 145 | processed.append(metadata.info) 146 | else: 147 | not_processed.append(metadata.info) 148 | 149 | # show a completion message 150 | msg = ptext.format(len(processed)) 151 | if should_convert and len(converted) > 0: 152 | msg += '\n' + convtext.format(lst2string(converted)) 153 | if len(not_processed) > 0: 154 | msg += '\n' + notptext.format(lst2string(not_processed)) 155 | info_dialog(ia.gui, title, msg, show=True) 156 | 157 | 158 | def get_selected_books(ia): 159 | # Get currently selected books 160 | rows = ia.gui.library_view.selectionModel().selectedRows() 161 | if not rows or len(rows) == 0: 162 | return error_dialog(ia.gui, _L['Cannot update metadata'], 163 | _L['No books selected'], show=True) 164 | # Map the rows to book ids 165 | return map(ia.gui.library_view.model().id, rows) 166 | 167 | 168 | def lst2string(lst): 169 | if python3: 170 | return "\n " + "\n ".join(lst) 171 | return "\n " + "\n ".join(item.encode('utf-8') for item in lst) 172 | 173 | 174 | def convert_to_cbz(ia, metadata): 175 | if metadata.format == "cbr" or (metadata.format == "rar" and prefs['convert_archives']): 176 | metadata.convert_cbr_to_cbz() 177 | if prefs['delete_cbr']: 178 | ia.gui.current_db.new_api.remove_formats({metadata.book_id: {"cbr", "rar"}}) 179 | return True 180 | elif metadata.format == "zip" and prefs['convert_archives']: 181 | metadata.convert_zip_to_cbz() 182 | if prefs['delete_cbr']: 183 | ia.gui.current_db.new_api.remove_formats({metadata.book_id: {"zip"}}) 184 | return True 185 | return False 186 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | from __future__ import (unicode_literals, division, absolute_import, 2 | print_function) 3 | 4 | __license__ = 'GPL v3' 5 | __copyright__ = '2015, dloraine' 6 | __docformat__ = 'restructuredtext en' 7 | 8 | try: 9 | from PyQt5.Qt import (QWidget, QCheckBox, QGridLayout, QVBoxLayout, 10 | QGroupBox, QComboBox, QLabel, QButtonGroup, QScrollArea) 11 | except ImportError: 12 | from PyQt4.Qt import (QWidget, QCheckBox, QGridLayout, QScrollArea, 13 | QVBoxLayout, QGroupBox, QComboBox, QLabel, QButtonGroup) 14 | 15 | from functools import partial 16 | 17 | from calibre.utils.config import JSONConfig 18 | from calibre_plugins.EmbedComicMetadata.ini import ( 19 | get_configuration, CONFIG_NAME, CONFIG_TITLE, CONFIG_DEFAULT, CONFIG_COLUMN_TYPE) 20 | 21 | import sys 22 | 23 | python3 = sys.version_info[0] > 2 24 | 25 | # python 2/3 compatibility 26 | def iteritems(d): 27 | if python3: 28 | return iter(d.items()) 29 | return iter(d.iteritems()) 30 | 31 | 32 | # This is where all preferences for this plugin will be stored 33 | # Remember that this name (i.e. plugins/interface_demo) is also 34 | # in a global namespace, so make it as unique as possible. 35 | # You should always prefix your config file name with plugins/, 36 | # so as to ensure you dont accidentally clobber a calibre config file 37 | prefs = JSONConfig('plugins/EmbedComicMetadata') 38 | 39 | config = get_configuration() 40 | 41 | # set defaults 42 | prefs.defaults = {item[CONFIG_NAME]: item[CONFIG_DEFAULT] 43 | for group in config for item in group["Items"]} 44 | 45 | 46 | class ConfigWidget(QWidget): 47 | 48 | def __init__(self, ia): 49 | QWidget.__init__(self) 50 | self.ia = ia 51 | self.layout = QVBoxLayout() 52 | self.setLayout(self.layout) 53 | 54 | # make the config menu as a widget 55 | self.config_menu = self.make_menu() 56 | 57 | # make a scroll area to hold the menu and add it to the layout 58 | self.scroll = QScrollArea() 59 | self.scroll.setWidget(self.config_menu) 60 | self.layout.addWidget(self.scroll) 61 | 62 | def save_settings(self): 63 | for group in config: 64 | if group["Type"] == "columnboxes": 65 | func = self.CustomColumnComboBox.get_selected_column 66 | else: 67 | func = QCheckBox.isChecked 68 | for item in group["Items"]: 69 | name = getattr(self, item[CONFIG_NAME]) 70 | prefs[item[CONFIG_NAME]] = func(name) 71 | # rebuild the menu 72 | self.ia.toggle_menu_items() 73 | 74 | def make_menu(self): 75 | config_menu = QWidget() 76 | self.l = QVBoxLayout() 77 | config_menu.setLayout(self.l) 78 | 79 | # add the menu items 80 | for group in config: 81 | self.make_submenu(group, self.l) 82 | 83 | return config_menu 84 | 85 | def make_submenu(self, group, parent): 86 | lo = self.make_groupbox(group, parent) 87 | 88 | # get the right builder function for the group type 89 | if group["Type"] == "checkboxes": 90 | func = partial(self.make_checkbox, lo, group["Columns"]) 91 | else: 92 | func = partial(self.make_columnbox, lo, group["Columns"]) 93 | 94 | # loop through the items and build the entries 95 | grid_row, grid_column = 1, 0 96 | for item in group["Items"]: 97 | grid_row, grid_column = func(item, grid_row, grid_column) 98 | 99 | # make buttons exclusive 100 | if "Exclusive_Items" in group: 101 | for item_list in group["Exclusive_Items"]: 102 | self.make_exclusive(item_list) 103 | 104 | def make_exclusive(self, item_list): 105 | excl_group = QButtonGroup(self) 106 | for item in item_list: 107 | item = getattr(self, item) 108 | excl_group.addButton(item) 109 | 110 | def make_groupbox(self, group, parent): 111 | groupbox = QGroupBox(group["Title"], self) 112 | setattr(self, group["Name"] + "_box", groupbox) 113 | parent.addWidget(groupbox) 114 | groupbox_layout = QGridLayout() 115 | setattr(self, group["Name"] + "_layout", groupbox_layout) 116 | groupbox.setLayout(groupbox_layout) 117 | return groupbox_layout 118 | 119 | def make_checkbox(self, parent, columns, item, grid_row, grid_column): 120 | checkbox = QCheckBox(item[CONFIG_TITLE], self) 121 | setattr(self, item[CONFIG_NAME], checkbox) 122 | checkbox.setChecked(prefs[item[CONFIG_NAME]]) 123 | parent.addWidget(checkbox, grid_row, grid_column) 124 | 125 | # check for new row 126 | if grid_column < columns - 1: 127 | return grid_row, grid_column + 1 128 | return grid_row + 1, 0 129 | 130 | def make_columnbox(self, parent, columns, item, grid_row, grid_column): 131 | # label 132 | column_label = QLabel(item[CONFIG_TITLE], self) 133 | setattr(self, item[CONFIG_NAME] + "label", column_label) 134 | 135 | # columnbox 136 | available_columns = self.get_custom_columns(item[CONFIG_COLUMN_TYPE]) 137 | column_box = self.CustomColumnComboBox(self, available_columns, prefs[item[CONFIG_NAME]]) 138 | setattr(self, item[CONFIG_NAME], column_box) 139 | 140 | # put together and add 141 | column_label.setBuddy(column_box) 142 | parent.addWidget(column_label, grid_row, grid_column) 143 | parent.addWidget(column_box, grid_row, grid_column + 1) 144 | 145 | # check for new row 146 | if grid_column < columns / 2: 147 | return grid_row, grid_column + 2 148 | return grid_row + 1, 0 149 | 150 | def get_custom_columns(self, column_type): 151 | ''' 152 | Gets matching custom columns for column_type 153 | ''' 154 | custom_columns = self.ia.gui.library_view.model().custom_columns 155 | available_columns = {} 156 | for key, column in iteritems(custom_columns): 157 | if (column["datatype"] in column_type["datatype"] and 158 | bool(column["is_multiple"]) == column_type["is_multiple"] and 159 | column['display'].get('is_names', False) == column_type['is_names']): 160 | available_columns[key] = column 161 | return available_columns 162 | 163 | # modified from CountPages 164 | class CustomColumnComboBox(QComboBox): 165 | 166 | def __init__(self, parent, custom_columns={}, selected_column=''): 167 | QComboBox.__init__(self, parent) 168 | self.populate_combo(custom_columns, selected_column) 169 | 170 | def populate_combo(self, custom_columns, selected_column): 171 | self.clear() 172 | self.column_names = [] 173 | selected_idx = 0 174 | custom_columns[""] = {"name": ""} 175 | for key in sorted(custom_columns.keys()): 176 | self.column_names.append(key) 177 | self.addItem('%s' % (custom_columns[key]['name'])) 178 | if key == selected_column: 179 | selected_idx = len(self.column_names) - 1 180 | self.setCurrentIndex(selected_idx) 181 | 182 | def select_column(self, key): 183 | selected_idx = 0 184 | for i, val in enumerate(self.column_names): 185 | if val == key: 186 | selected_idx = i 187 | break 188 | self.setCurrentIndex(selected_idx) 189 | 190 | def get_selected_column(self): 191 | return self.column_names[self.currentIndex()] 192 | -------------------------------------------------------------------------------- /ini.py: -------------------------------------------------------------------------------- 1 | from __future__ import (unicode_literals, division, absolute_import, 2 | print_function) 3 | 4 | __license__ = 'GPL v3' 5 | __copyright__ = '2015, dloraine' 6 | __docformat__ = 'restructuredtext en' 7 | 8 | 9 | # Define some column types 10 | PERSON_TYPE = {"is_multiple": True, "is_names": True, "datatype": "text"} 11 | TAG_TYPE = {"is_multiple": True, "is_names": False, "datatype": "text"} 12 | FLOAT_TYPE = {"is_multiple": False, "is_names": False, "datatype": "float"} 13 | INT_TYPE = {"is_multiple": False, "is_names": False, "datatype": "int"} 14 | COMMENT_TYPE = {"is_multiple": False, "is_names": False, "datatype": "comments"} 15 | SERIES_TYPE = {"is_multiple": False, "is_names": False, "datatype": "series"} 16 | STORY_ARCH_TYPE = {"is_multiple": False, "is_names": False, "datatype": ["series", "text"]} 17 | NUMBER_TYPE = {"is_multiple": False, "is_names": False, "datatype": ["int", "text"]} 18 | ENUM_TYPE = {"is_multiple": False, "is_names": False, "datatype": ["enumeration", "text"]} 19 | 20 | # Some constants for ease of reading 21 | CONFIG_NAME = 0 22 | CONFIG_TITLE = 1 23 | CONFIG_DEFAULT = 2 24 | CONFIG_COLUMN_TYPE = 3 25 | CONFIG_DESCRIPTION = 1 26 | CONFIG_TRIGGER_FUNC = 2 27 | CONFIG_TRIGGER_ARG = 3 28 | CONFIG_ARTISTS_COLUMNS = 0 29 | CONFIG_OTHER_COLUMNS = 1 30 | CONFIG_OPTIONS = 2 31 | CONFIG_MAIN_BUTTON = 3 32 | CONFIG_MENU = 4 33 | 34 | 35 | def get_configuration(): 36 | ''' 37 | All configuration for preferences and the UI-Menu is made with the 38 | informations given here. No need to change anything in config.py or ui.py, 39 | if new preferences or menu items need to be made. 40 | 41 | Name: The internal name for the preference group 42 | Title: The Label in the config menu 43 | Type: Either "columnboxes" or "checkboxes" 44 | Columns: How many columns the group should have in the config menu 45 | Items: The preferences in the menu group. Has the form: 46 | [preference_name, displayed name, default_state, if column: columntype] 47 | UI_Action_Items: The buttons in the toolbar menu. Has the form: 48 | [name, displayed_text, triggerfunc, triggerfunc_arg] 49 | ''' 50 | from calibre_plugins.EmbedComicMetadata.languages.lang import _L 51 | from calibre_plugins.EmbedComicMetadata.main import (embed_into_comic, 52 | import_to_calibre, embed_cover, convert, count_pages, get_image_size, remove_metadata) 53 | 54 | # configuration 55 | config = [ 56 | { 57 | "Name": "artists_custom_columns", 58 | "Title": _L["Artists Custom Columns:"], 59 | "Type": "columnboxes", 60 | "Columns": 2, 61 | "Items": [ 62 | ["penciller_column", _L['Penciller:'], None, PERSON_TYPE], 63 | ["inker_column", _L['Inker:'], None, PERSON_TYPE], 64 | ["colorist_column", _L['Colorist:'], None, PERSON_TYPE], 65 | ["letterer_column", _L['Letterer:'], None, PERSON_TYPE], 66 | ["cover_artist_column", _L['Cover Artist:'], None, PERSON_TYPE], 67 | ["editor_column", _L['Editor:'], None, PERSON_TYPE] 68 | ] 69 | }, 70 | { 71 | "Name": "other_custom_columns", 72 | "Title": _L["Other Custom Columns:"], 73 | "Type": "columnboxes", 74 | "Columns": 2, 75 | "Items": [ 76 | ["storyarc_column", _L['Story Arc:'], None, STORY_ARCH_TYPE], 77 | ["characters_column", _L['Characters:'], None, TAG_TYPE], 78 | ["teams_column", _L['Teams:'], None, TAG_TYPE], 79 | ["locations_column", _L['Locations:'], None, TAG_TYPE], 80 | ["volume_column", _L['Volume:'], None, NUMBER_TYPE], 81 | ["genre_column", _L['Genre:'], None, TAG_TYPE], 82 | ["count_column", _L['Number of issues:'], None, NUMBER_TYPE], 83 | ["pages_column", _L['Pages:'], None, NUMBER_TYPE], 84 | ["image_size_column", _L['Image size:'], None, FLOAT_TYPE], 85 | ["comicvine_column", _L['Comicvine link:'], None, COMMENT_TYPE], 86 | ["manga_column", _L['Manga:'], None, ENUM_TYPE] 87 | ] 88 | }, 89 | { 90 | "Name": "options", 91 | "Title": _L["Options:"], 92 | "Type": "checkboxes", 93 | "Columns": 2, 94 | "Items": [ 95 | ["cbi_embed", _L['Write metadata in zip comment'], True], 96 | ["cix_embed", _L['Write metadata in ComicInfo.xml'], True], 97 | ["read_cbi", _L['Import metadata from zip comment'], True], 98 | ["read_cix", _L['Import metadata from ComicInfo.xml'], True], 99 | ["convert_cbr", _L['Auto convert cbr to cbz'], True], 100 | ["convert_archives", _L['Also convert rar and zip to cbz'], False], 101 | ["convert_reading", _L['Auto convert while importing to calibre'], False], 102 | ["delete_cbr", _L['Delete cbr after conversion'], False], 103 | ["swap_names", _L['Swap names to "LN, FN" when importing metadata'], False], 104 | ["import_tags", _L['Import tags from comic metadata'], False], 105 | ["overwrite_calibre_tags", _L['If checked, overwrites the tags in calibre.'], False], 106 | ["auto_count_pages", _L['Auto count pages if importing'], False], 107 | ["get_image_sizes", _L['Get the image size if importing'], False] 108 | ] 109 | }, 110 | { 111 | "Name": "main_button", 112 | "Title": _L["Main Button Action (needs a calibre restart):"], 113 | "Type": "checkboxes", 114 | "Columns": 2, 115 | "Items": [ 116 | ["main_embed", _L['Embed metadata'], True], 117 | ["main_import", _L['Import metadata'], False], 118 | ], 119 | "Exclusive_Items": [ 120 | ["main_embed", "main_import"] 121 | ] 122 | }, 123 | { 124 | "Name": "menu", 125 | "Title": _L["Menu Buttons:"], 126 | "Type": "checkboxes", 127 | "Columns": 2, 128 | "Items": [ 129 | ["embed", _L['Show embed both button'], True], 130 | ["embedcbi", _L['Show embed cbi button'], False], 131 | ["embedcix", _L['Show embed cix button'], False], 132 | ["read_both", _L['Show import both button'], True], 133 | ["import_cix", _L['Show import cix button'], False], 134 | ["import_cbi", _L['Show import cbi button'], False], 135 | ["convert", _L['Show convert button'], True], 136 | ["count_pages", _L['Show count pages button'], True], 137 | ["image_size", _L['Show get image size button'], False], 138 | ["remove_metadata", _L['Show remove metadata button'], False], 139 | ["cover", _L['Show embed cover button (experimental)'], False] 140 | ], 141 | "UI_Action_Items": [ 142 | ["read_both", _L['Import Metadata from the comic archive into calibre'], import_to_calibre, "both"], 143 | ["import_cix", _L["Import Comic Rack Metadata from the comic archive into calibre"], import_to_calibre, "cix"], 144 | ["import_cbi", _L["Import Comment Metadata from the comic archive into calibre"], import_to_calibre, "cbi"], 145 | ["seperator"], 146 | ["embed", _L["Embed both Comic Metadata types"], embed_into_comic, "both"], 147 | ["embedcbi", _L["Only embed Metadata in zip comment"], embed_into_comic, "cbi"], 148 | ["embedcix", _L["Only embed Metadata in ComicInfo.xml"], embed_into_comic, "cix"], 149 | ["seperator"], 150 | ["convert", _L["Only convert to cbz"], convert, None], 151 | ["cover", _L["Embed the calibre cover"], embed_cover, None], 152 | ["count_pages", _L["Count pages"], count_pages, None], 153 | ["image_size", _L["Get image size"], get_image_size, None], 154 | ["remove_metadata", _L["Remove metadata"], remove_metadata, None], 155 | ["seperator"] 156 | ] 157 | } 158 | ] 159 | 160 | return config 161 | -------------------------------------------------------------------------------- /test-config/gui.json: -------------------------------------------------------------------------------- 1 | { 2 | "action-layout-toolbar": [ 3 | "Add Books", 4 | "Edit Metadata", 5 | null, 6 | "Convert Books", 7 | "View", 8 | null, 9 | "Store", 10 | "Donate", 11 | "Fetch News", 12 | "Help", 13 | null, 14 | "Preferences", 15 | "Remove Books", 16 | "Choose Library", 17 | "Save To Disk", 18 | "Connect Share", 19 | "Tweak ePub", 20 | "Embed Comic Metadata" 21 | ], 22 | "basic_metadata_widget_splitter_state": { 23 | "__class__": "bytearray", 24 | "__value__": "AAAA/wAAAAEAAAADAAAA9wAAAXoAAAEKAf////8BAAAAAQA=" 25 | }, 26 | "book_list_pin_splitter_state": { 27 | "__class__": "bytearray", 28 | "__value__": "AAAA/wAAAAEAAAACAAABAAAAAEYA/////wEAAAABAA==" 29 | }, 30 | "cover_grid_background": { 31 | "dark": [ 32 | 45, 33 | 45, 34 | 45 35 | ], 36 | "dark_texture": null, 37 | "light": [ 38 | 80, 39 | 80, 40 | 80 41 | ], 42 | "light_texture": null, 43 | "migrated": true 44 | }, 45 | "custom_colors_for_color_dialog": [ 46 | [ 47 | 255, 48 | 255, 49 | 255, 50 | 255 51 | ], 52 | [ 53 | 255, 54 | 255, 55 | 255, 56 | 255 57 | ], 58 | [ 59 | 255, 60 | 255, 61 | 255, 62 | 255 63 | ], 64 | [ 65 | 255, 66 | 255, 67 | 255, 68 | 255 69 | ], 70 | [ 71 | 255, 72 | 255, 73 | 255, 74 | 255 75 | ], 76 | [ 77 | 255, 78 | 255, 79 | 255, 80 | 255 81 | ], 82 | [ 83 | 255, 84 | 255, 85 | 255, 86 | 255 87 | ], 88 | [ 89 | 255, 90 | 255, 91 | 255, 92 | 255 93 | ], 94 | [ 95 | 255, 96 | 255, 97 | 255, 98 | 255 99 | ], 100 | [ 101 | 255, 102 | 255, 103 | 255, 104 | 255 105 | ], 106 | [ 107 | 255, 108 | 255, 109 | 255, 110 | 255 111 | ], 112 | [ 113 | 255, 114 | 255, 115 | 255, 116 | 255 117 | ], 118 | [ 119 | 255, 120 | 255, 121 | 255, 122 | 255 123 | ], 124 | [ 125 | 255, 126 | 255, 127 | 255, 128 | 255 129 | ], 130 | [ 131 | 255, 132 | 255, 133 | 255, 134 | 255 135 | ], 136 | [ 137 | 255, 138 | 255, 139 | 255, 140 | 255 141 | ] 142 | ], 143 | "geometry-of-calibre_main_window_geometry": { 144 | "frame_geometry": { 145 | "height": 1032, 146 | "width": 1920, 147 | "x": -3, 148 | "y": -30 149 | }, 150 | "full_screened": false, 151 | "geometry": { 152 | "height": 999, 153 | "width": 1914, 154 | "x": 0, 155 | "y": 0 156 | }, 157 | "maximized": true, 158 | "normal_geometry": { 159 | "height": 999, 160 | "width": 1914, 161 | "x": 0, 162 | "y": 0 163 | }, 164 | "qt": { 165 | "__class__": "bytearray", 166 | "__value__": "AdnQywADAAD////9////4gAAB3wAAAPpAAAAAAAAAAAAAAd5AAAD5gAAAAACAAAAB4AAAAAAAAAAAAAAB3kAAAPm" 167 | }, 168 | "screen": { 169 | "depth": 32, 170 | "device_pixel_ratio": 1.0, 171 | "geometry_in_logical_pixels": { 172 | "height": 1080, 173 | "width": 1920, 174 | "x": 0, 175 | "y": 0 176 | }, 177 | "index_in_screens_list": 0, 178 | "manufacturer": "weston", 179 | "model": "rdp", 180 | "name": "rdp-0", 181 | "serial": "", 182 | "size_in_logical_pixels": { 183 | "height": 1080, 184 | "width": 1920 185 | }, 186 | "virtual_geometry": { 187 | "height": 1080, 188 | "width": 3840, 189 | "x": 0, 190 | "y": 0 191 | } 192 | } 193 | }, 194 | "geometry-of-metasingle_window_geometry3": { 195 | "frame_geometry": { 196 | "height": 1063, 197 | "width": 1856, 198 | "x": -3, 199 | "y": -30 200 | }, 201 | "full_screened": false, 202 | "geometry": { 203 | "height": 1030, 204 | "width": 1850, 205 | "x": 0, 206 | "y": 0 207 | }, 208 | "maximized": false, 209 | "normal_geometry": { 210 | "height": 1030, 211 | "width": 1850, 212 | "x": 0, 213 | "y": 0 214 | }, 215 | "qt": { 216 | "__class__": "bytearray", 217 | "__value__": "AdnQywADAAD////9////4gAABzwAAAQIAAAAAAAAAAAAAAc5AAAEBQAAAAAAAAAAB4AAAAAAAAAAAAAABzkAAAQF" 218 | }, 219 | "screen": { 220 | "depth": 32, 221 | "device_pixel_ratio": 1.0, 222 | "geometry_in_logical_pixels": { 223 | "height": 1080, 224 | "width": 1920, 225 | "x": 0, 226 | "y": 0 227 | }, 228 | "index_in_screens_list": 0, 229 | "manufacturer": "weston", 230 | "model": "rdp", 231 | "name": "rdp-0", 232 | "serial": "", 233 | "size_in_logical_pixels": { 234 | "height": 1080, 235 | "width": 1920 236 | }, 237 | "virtual_geometry": { 238 | "height": 1080, 239 | "width": 3840, 240 | "x": 0, 241 | "y": 0 242 | } 243 | } 244 | }, 245 | "geometry-of-plugin config dialog:User interface action:Embed Comic Metadata": { 246 | "frame_geometry": { 247 | "height": 465, 248 | "width": 766, 249 | "x": -3, 250 | "y": -30 251 | }, 252 | "full_screened": false, 253 | "geometry": { 254 | "height": 432, 255 | "width": 760, 256 | "x": 0, 257 | "y": 0 258 | }, 259 | "maximized": false, 260 | "normal_geometry": { 261 | "height": 432, 262 | "width": 760, 263 | "x": 0, 264 | "y": 0 265 | }, 266 | "qt": { 267 | "__class__": "bytearray", 268 | "__value__": "AdnQywADAAD////9////4gAAAvoAAAGyAAAAAAAAAAAAAAL3AAABrwAAAAAAAAAAB4AAAAAAAAAAAAAAAvcAAAGv" 269 | }, 270 | "screen": { 271 | "depth": 32, 272 | "device_pixel_ratio": 1.0, 273 | "geometry_in_logical_pixels": { 274 | "height": 1080, 275 | "width": 1920, 276 | "x": 0, 277 | "y": 0 278 | }, 279 | "index_in_screens_list": 0, 280 | "manufacturer": "weston", 281 | "model": "rdp", 282 | "name": "rdp-0", 283 | "serial": "", 284 | "size_in_logical_pixels": { 285 | "height": 1080, 286 | "width": 1920 287 | }, 288 | "virtual_geometry": { 289 | "height": 1080, 290 | "width": 3840, 291 | "x": 0, 292 | "y": 0 293 | } 294 | } 295 | }, 296 | "geometry-of-preferences dialog geometry": { 297 | "frame_geometry": { 298 | "height": 753, 299 | "width": 936, 300 | "x": 486, 301 | "y": 79 302 | }, 303 | "full_screened": false, 304 | "geometry": { 305 | "height": 720, 306 | "width": 930, 307 | "x": 489, 308 | "y": 109 309 | }, 310 | "maximized": false, 311 | "normal_geometry": { 312 | "height": 720, 313 | "width": 930, 314 | "x": 489, 315 | "y": 109 316 | }, 317 | "qt": { 318 | "__class__": "bytearray", 319 | "__value__": "AdnQywADAAAAAAHmAAAATwAABY0AAAM/AAAB6QAAAG0AAAWKAAADPAAAAAAAAAAAB4AAAAHpAAAAbQAABYoAAAM8" 320 | }, 321 | "screen": { 322 | "depth": 32, 323 | "device_pixel_ratio": 1.0, 324 | "geometry_in_logical_pixels": { 325 | "height": 1080, 326 | "width": 1920, 327 | "x": 0, 328 | "y": 0 329 | }, 330 | "index_in_screens_list": 0, 331 | "manufacturer": "weston", 332 | "model": "rdp", 333 | "name": "rdp-0", 334 | "serial": "", 335 | "size_in_logical_pixels": { 336 | "height": 1080, 337 | "width": 1920 338 | }, 339 | "virtual_geometry": { 340 | "height": 1080, 341 | "width": 3840, 342 | "x": 0, 343 | "y": 0 344 | } 345 | } 346 | }, 347 | "grid view visible": false, 348 | "library_usage_stats": { 349 | "/root/Calibre Library": 12 350 | }, 351 | "main_window_central_widget_state": { 352 | "layout": "wide", 353 | "narrow_desires": { 354 | "book_details_height": 0.23042505592841164, 355 | "cover_browser_width": 0.34953538241601145, 356 | "quick_view_height": 0.25615212527964204, 357 | "tag_browser_width": 0.2494639027877055 358 | }, 359 | "narrow_visibility": { 360 | "book_details": true, 361 | "book_list": true, 362 | "cover_browser": false, 363 | "quick_view": false, 364 | "tag_browser": true 365 | }, 366 | "wide_desires": { 367 | "book_details_width": 0.18084345961401, 368 | "cover_browser_height": 0.28556593977154726, 369 | "quick_view_height": 0.23779854620976115, 370 | "tag_browser_width": 0.1501072194424589 371 | }, 372 | "wide_visibility": { 373 | "book_details": true, 374 | "book_list": true, 375 | "cover_browser": false, 376 | "quick_view": false, 377 | "tag_browser": true 378 | } 379 | }, 380 | "quick_start_guide_added": true, 381 | "qv_open_at_shutdown": false, 382 | "recently_used_languages": [ 383 | "English" 384 | ], 385 | "search bar visible": true, 386 | "tag browser search box visible": false 387 | } -------------------------------------------------------------------------------- /genericmetadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python class for internal metadata storage 3 | 4 | The goal of this class is to handle ALL the data that might come from various 5 | tagging schemes and databases, such as ComicVine or GCD. This makes conversion 6 | possible, however lossy it might be 7 | 8 | """ 9 | 10 | """ 11 | Copyright 2012-2014 Anthony Beville 12 | 13 | Licensed under the Apache License, Version 2.0 (the "License"); 14 | you may not use this file except in compliance with the License. 15 | You may obtain a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, 21 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | See the License for the specific language governing permissions and 23 | limitations under the License. 24 | """ 25 | 26 | 27 | # These page info classes are exactly the same as the CIX scheme, since it's unique 28 | class PageType: 29 | FrontCover = "FrontCover" 30 | InnerCover = "InnerCover" 31 | Roundup = "Roundup" 32 | Story = "Story" 33 | Advertisement = "Advertisement" 34 | Editorial = "Editorial" 35 | Letters = "Letters" 36 | Preview = "Preview" 37 | BackCover = "BackCover" 38 | Other = "Other" 39 | Deleted = "Deleted" 40 | 41 | 42 | class GenericMetadata: 43 | 44 | def __init__(self): 45 | 46 | self.isEmpty = True 47 | self.tagOrigin = None 48 | 49 | self.series = None 50 | self.issue = None 51 | self.title = None 52 | self.publisher = None 53 | self.month = None 54 | self.year = None 55 | self.day = None 56 | self.issueCount = None 57 | self.volume = None 58 | self.genre = None 59 | self.language = None # 2 letter iso code 60 | self.comments = None # use same way as Summary in CIX 61 | 62 | self.volumeCount = None 63 | self.criticalRating = None 64 | self.country = None 65 | 66 | self.alternateSeries = None 67 | self.alternateNumber = None 68 | self.alternateCount = None 69 | self.imprint = None 70 | self.notes = None 71 | self.webLink = None 72 | self.format = None 73 | self.manga = None 74 | self.blackAndWhite = None 75 | self.pageCount = None 76 | self.maturityRating = None 77 | 78 | self.storyArc = None 79 | self.seriesGroup = None 80 | self.scanInfo = None 81 | 82 | self.characters = None 83 | self.teams = None 84 | self.locations = None 85 | 86 | self.credits = list() 87 | self.tags = list() 88 | self.pages = list() 89 | 90 | # Some CoMet-only items 91 | self.price = None 92 | self.isVersionOf = None 93 | self.rights = None 94 | self.identifier = None 95 | self.lastMark = None 96 | self.coverImage = None 97 | 98 | # Anansi Project extensions 99 | self.gtin = None 100 | 101 | def overlay(self, new_md, overwrite=True): # changed for the calibre plugin 102 | # Overlay a metadata object on this one 103 | # that is, when the new object has non-None 104 | # values, over-write them to this one 105 | 106 | def assign(cur, new): 107 | if new is not None: 108 | if type(new) == str and len(new) == 0: 109 | if overwrite: 110 | setattr(self, cur, None) 111 | else: 112 | setattr(self, cur, new) 113 | 114 | if not new_md.isEmpty: 115 | self.isEmpty = False 116 | 117 | assign('series', new_md.series) 118 | assign("issue", new_md.issue) 119 | assign("issueCount", new_md.issueCount) 120 | assign("title", new_md.title) 121 | assign("publisher", new_md.publisher) 122 | assign("day", new_md.day) 123 | assign("month", new_md.month) 124 | assign("year", new_md.year) 125 | assign("volume", new_md.volume) 126 | assign("volumeCount", new_md.volumeCount) 127 | assign("genre", new_md.genre) 128 | assign("language", new_md.language) 129 | assign("country", new_md.country) 130 | assign("criticalRating", new_md.criticalRating) 131 | assign("alternateSeries", new_md.alternateSeries) 132 | assign("alternateNumber", new_md.alternateNumber) 133 | assign("alternateCount", new_md.alternateCount) 134 | assign("imprint", new_md.imprint) 135 | assign("webLink", new_md.webLink) 136 | assign("format", new_md.format) 137 | assign("manga", new_md.manga) 138 | assign("blackAndWhite", new_md.blackAndWhite) 139 | assign("maturityRating", new_md.maturityRating) 140 | assign("storyArc", new_md.storyArc) 141 | assign("seriesGroup", new_md.seriesGroup) 142 | assign("scanInfo", new_md.scanInfo) 143 | assign("characters", new_md.characters) 144 | assign("teams", new_md.teams) 145 | assign("locations", new_md.locations) 146 | assign("comments", new_md.comments) 147 | assign("notes", new_md.notes) 148 | assign("pageCount", new_md.pageCount) 149 | 150 | assign("price", new_md.price) 151 | assign("isVersionOf", new_md.isVersionOf) 152 | assign("rights", new_md.rights) 153 | assign("identifier", new_md.identifier) 154 | assign("lastMark", new_md.lastMark) 155 | assign("gtin", new_md.gtin) 156 | 157 | self.overlayCredits(new_md.credits, overwrite) 158 | # TODO 159 | 160 | # not sure if the tags and pages should broken down, or treated 161 | # as whole lists.... 162 | 163 | # For now, go the easy route, where any overlay 164 | # value wipes out the whole list 165 | if len(new_md.tags) > 0: 166 | assign("tags", new_md.tags) 167 | 168 | if len(new_md.pages) > 0: 169 | assign("pages", new_md.pages) 170 | 171 | # modified for calibre, deletes old writers 172 | def overlayCredits(self, new_credits, overwrite=True): 173 | # changed for the calibre plugin 174 | if overwrite: 175 | roles_to_remove = set(c["role"].lower() for c in new_credits) 176 | self.credits = [r for r in self.credits 177 | if r["role"].lower() not in roles_to_remove] 178 | 179 | for c in new_credits: 180 | if 'primary' in c and c['primary']: 181 | primary = True 182 | else: 183 | primary = False 184 | 185 | # Remove credit role if person is blank 186 | if c['person'] == "": 187 | for r in reversed(self.credits): 188 | if r['role'].lower() == c['role'].lower(): 189 | self.credits.remove(r) 190 | 191 | # otherwise, add it! 192 | else: 193 | self.addCredit(c['person'], c['role'], primary) 194 | 195 | def setDefaultPageList(self, count): 196 | # generate a default page list, with the first page marked as the cover 197 | for i in range(count): 198 | page_dict = dict() 199 | page_dict['Image'] = str(i) 200 | if i == 0: 201 | page_dict['Type'] = PageType.FrontCover 202 | self.pages.append(page_dict) 203 | 204 | def getArchivePageIndex(self, pagenum): 205 | # convert the displayed page number to the page index of the file in the archive 206 | if pagenum < len(self.pages): 207 | return int(self.pages[pagenum]['Image']) 208 | else: 209 | return 0 210 | 211 | def getCoverPageIndexList(self): 212 | # return a list of archive page indices of cover pages 213 | coverlist = [] 214 | for p in self.pages: 215 | if 'Type' in p and p['Type'] == PageType.FrontCover: 216 | coverlist.append(int(p['Image'])) 217 | 218 | if len(coverlist) == 0: 219 | coverlist.append(0) 220 | 221 | return coverlist 222 | 223 | def addCredit(self, person, role, primary=False): 224 | 225 | credit = dict() 226 | credit['person'] = person 227 | credit['role'] = role 228 | if primary: 229 | credit['primary'] = primary 230 | 231 | # look to see if it's not already there... 232 | found = False 233 | for c in self.credits: 234 | if (c['person'].lower().strip() == person.lower().strip() and 235 | c['role'].lower() == role.lower()): 236 | # no need to add it. just adjust the "primary" flag as needed 237 | c['primary'] = primary 238 | found = True 239 | break 240 | 241 | if not found: 242 | self.credits.append(credit) 243 | 244 | def __str__(self): 245 | vals = [] 246 | if self.isEmpty: 247 | return "No metadata" 248 | 249 | def add_string(tag, val): 250 | if val is not None and u"{0}".format(val) != "": 251 | vals.append((tag, val)) 252 | 253 | def add_attr_string(tag): 254 | add_string(tag, getattr(self, tag)) 255 | 256 | add_attr_string("series") 257 | add_attr_string("issue") 258 | add_attr_string("issueCount") 259 | add_attr_string("title") 260 | add_attr_string("publisher") 261 | add_attr_string("year") 262 | add_attr_string("month") 263 | add_attr_string("day") 264 | add_attr_string("volume") 265 | add_attr_string("volumeCount") 266 | add_attr_string("genre") 267 | add_attr_string("language") 268 | add_attr_string("country") 269 | add_attr_string("criticalRating") 270 | add_attr_string("alternateSeries") 271 | add_attr_string("alternateNumber") 272 | add_attr_string("alternateCount") 273 | add_attr_string("imprint") 274 | add_attr_string("webLink") 275 | add_attr_string("format") 276 | add_attr_string("manga") 277 | add_attr_string("pageCount") 278 | 279 | add_attr_string("price") 280 | add_attr_string("isVersionOf") 281 | add_attr_string("rights") 282 | add_attr_string("identifier") 283 | add_attr_string("lastMark") 284 | 285 | if self.blackAndWhite: 286 | add_attr_string("blackAndWhite") 287 | add_attr_string("maturityRating") 288 | add_attr_string("storyArc") 289 | add_attr_string("seriesGroup") 290 | add_attr_string("scanInfo") 291 | add_attr_string("characters") 292 | add_attr_string("teams") 293 | add_attr_string("locations") 294 | add_attr_string("comments") 295 | add_attr_string("notes") 296 | 297 | add_string("tags", listToString(self.tags)) 298 | 299 | for c in self.credits: 300 | primary = "" 301 | if 'primary' in c and c['primary']: 302 | primary = " [P]" 303 | add_string("credit", c['role'] + ": " + c['person'] + primary) 304 | 305 | # find the longest field name 306 | flen = 0 307 | for i in vals: 308 | flen = max(flen, len(i[0])) 309 | flen += 1 310 | 311 | # format the data nicely 312 | outstr = "" 313 | fmt_str = u"{0: <" + str(flen) + "} {1}\n" 314 | for i in vals: 315 | outstr += fmt_str.format(i[0] + ":", i[1]) 316 | 317 | return outstr 318 | 319 | 320 | def listToString(l): 321 | string = "" 322 | if l is not None: 323 | for item in l: 324 | if len(string) > 0: 325 | string += ", " 326 | string += item 327 | return string 328 | -------------------------------------------------------------------------------- /comicinfoxml.py: -------------------------------------------------------------------------------- 1 | """ 2 | A python class to encapsulate ComicRack's ComicInfo.xml data 3 | """ 4 | 5 | """ 6 | Copyright 2012-2014 Anthony Beville 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | 21 | from pprint import pprint 22 | import xml.etree.ElementTree as ET 23 | from calibre_plugins.EmbedComicMetadata.genericmetadata import GenericMetadata 24 | 25 | import sys 26 | 27 | if sys.version_info[0] > 2: 28 | python3 = True 29 | unicode = str 30 | else: 31 | python3 = False 32 | 33 | 34 | class ComicInfoXml: 35 | 36 | writer_synonyms = ['writer', 'plotter', 'scripter'] 37 | penciller_synonyms = ['artist', 'penciller', 'penciler', 'breakdowns'] 38 | inker_synonyms = ['inker', 'artist', 'finishes'] 39 | colorist_synonyms = ['colorist', 'colourist', 'colorer', 'colourer'] 40 | letterer_synonyms = ['letterer'] 41 | cover_synonyms = ['cover', 'covers', 'coverartist', 'cover artist'] 42 | editor_synonyms = ['editor'] 43 | 44 | def getParseableCredits(self): 45 | parsable_credits = [] 46 | parsable_credits.extend(self.writer_synonyms) 47 | parsable_credits.extend(self.penciller_synonyms) 48 | parsable_credits.extend(self.inker_synonyms) 49 | parsable_credits.extend(self.colorist_synonyms) 50 | parsable_credits.extend(self.letterer_synonyms) 51 | parsable_credits.extend(self.cover_synonyms) 52 | parsable_credits.extend(self.editor_synonyms) 53 | return parsable_credits 54 | 55 | def metadataFromString(self, string): 56 | 57 | tree = ET.ElementTree(ET.fromstring(string)) 58 | return self.convertXMLToMetadata(tree) 59 | 60 | def stringFromMetadata(self, metadata): 61 | 62 | header = '<?xml version="1.0"?>\n' 63 | 64 | tree = self.convertMetadataToXML(self, metadata) 65 | if python3: 66 | return header + ET.tostring(tree.getroot(), "unicode") 67 | return header + ET.tostring(tree.getroot()) 68 | 69 | def indent(self, elem, level=0): 70 | # for making the XML output readable 71 | i = "\n" + level * " " 72 | if len(elem): 73 | if not elem.text or not elem.text.strip(): 74 | elem.text = i + " " 75 | if not elem.tail or not elem.tail.strip(): 76 | elem.tail = i 77 | for elem in elem: 78 | self.indent(elem, level + 1) 79 | if not elem.tail or not elem.tail.strip(): 80 | elem.tail = i 81 | else: 82 | if level and (not elem.tail or not elem.tail.strip()): 83 | elem.tail = i 84 | 85 | def convertMetadataToXML(self, filename, metadata): 86 | 87 | # shorthand for the metadata 88 | md = metadata 89 | 90 | # build a tree structure 91 | root = ET.Element("ComicInfo") 92 | root.attrib['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" 93 | root.attrib['xmlns:xsd'] = "http://www.w3.org/2001/XMLSchema" 94 | # helper func 95 | 96 | def assign(cix_entry, md_entry): 97 | if md_entry is not None: 98 | ET.SubElement(root, cix_entry).text = u"{0}".format(md_entry) 99 | 100 | assign('Title', md.title) 101 | assign('Series', md.series) 102 | assign('Number', md.issue) 103 | assign('Count', md.issueCount) 104 | assign('Volume', md.volume) 105 | assign('AlternateSeries', md.alternateSeries) 106 | assign('AlternateNumber', md.alternateNumber) 107 | assign('StoryArc', md.storyArc) 108 | assign('SeriesGroup', md.seriesGroup) 109 | assign('AlternateCount', md.alternateCount) 110 | assign('Summary', md.comments) 111 | assign('Notes', md.notes) 112 | assign('Year', md.year) 113 | assign('Month', md.month) 114 | assign('Day', md.day) 115 | 116 | # need to specially process the credits, since they are structured differently than CIX 117 | credit_writer_list = list() 118 | credit_penciller_list = list() 119 | credit_inker_list = list() 120 | credit_colorist_list = list() 121 | credit_letterer_list = list() 122 | credit_cover_list = list() 123 | credit_editor_list = list() 124 | 125 | # first, loop thru credits, and build a list for each role that CIX supports 126 | for credit in metadata.credits: 127 | 128 | if credit['role'].lower() in set(self.writer_synonyms): 129 | credit_writer_list.append(credit['person'].replace(",", "")) 130 | 131 | if credit['role'].lower() in set(self.penciller_synonyms): 132 | credit_penciller_list.append(credit['person'].replace(",", "")) 133 | 134 | if credit['role'].lower() in set(self.inker_synonyms): 135 | credit_inker_list.append(credit['person'].replace(",", "")) 136 | 137 | if credit['role'].lower() in set(self.colorist_synonyms): 138 | credit_colorist_list.append(credit['person'].replace(",", "")) 139 | 140 | if credit['role'].lower() in set(self.letterer_synonyms): 141 | credit_letterer_list.append(credit['person'].replace(",", "")) 142 | 143 | if credit['role'].lower() in set(self.cover_synonyms): 144 | credit_cover_list.append(credit['person'].replace(",", "")) 145 | 146 | if credit['role'].lower() in set(self.editor_synonyms): 147 | credit_editor_list.append(credit['person'].replace(",", "")) 148 | 149 | # second, convert each list to string, and add to XML struct 150 | if len(credit_writer_list) > 0: 151 | node = ET.SubElement(root, 'Writer') 152 | node.text = listToString(credit_writer_list) 153 | 154 | if len(credit_penciller_list) > 0: 155 | node = ET.SubElement(root, 'Penciller') 156 | node.text = listToString(credit_penciller_list) 157 | 158 | if len(credit_inker_list) > 0: 159 | node = ET.SubElement(root, 'Inker') 160 | node.text = listToString(credit_inker_list) 161 | 162 | if len(credit_colorist_list) > 0: 163 | node = ET.SubElement(root, 'Colorist') 164 | node.text = listToString(credit_colorist_list) 165 | 166 | if len(credit_letterer_list) > 0: 167 | node = ET.SubElement(root, 'Letterer') 168 | node.text = listToString(credit_letterer_list) 169 | 170 | if len(credit_cover_list) > 0: 171 | node = ET.SubElement(root, 'CoverArtist') 172 | node.text = listToString(credit_cover_list) 173 | 174 | if len(credit_editor_list) > 0: 175 | node = ET.SubElement(root, 'Editor') 176 | node.text = listToString(credit_editor_list) 177 | 178 | # calibre custom columns like tags return tuples, so we need to handle 179 | # these specially 180 | md.characters = tuple_to_string(md.characters) 181 | md.teams = tuple_to_string(md.teams) 182 | md.locations = tuple_to_string(md.locations) 183 | md.genre = tuple_to_string(md.genre) 184 | md.tags = tuple_to_string(md.tags) 185 | 186 | assign('Publisher', md.publisher) 187 | assign('Imprint', md.imprint) 188 | assign('Genre', md.genre) 189 | if md.tags: 190 | assign('Tags', md.tags) 191 | assign('Web', md.webLink) 192 | assign('PageCount', md.pageCount) 193 | assign('LanguageISO', md.language) 194 | assign('Format', md.format) 195 | assign('AgeRating', md.maturityRating) 196 | if md.blackAndWhite is not None and md.blackAndWhite: 197 | ET.SubElement(root, 'BlackAndWhite').text = "Yes" 198 | assign('Manga', md.manga) 199 | assign('Characters', md.characters) 200 | assign('Teams', md.teams) 201 | assign('Locations', md.locations) 202 | assign('ScanInformation', md.scanInfo) 203 | assign('GTIN', md.gtin) 204 | 205 | # loop and add the page entries under pages node 206 | if len(md.pages) > 0: 207 | pages_node = ET.SubElement(root, 'Pages') 208 | for page_dict in md.pages: 209 | page_node = ET.SubElement(pages_node, 'Page') 210 | page_node.attrib = page_dict 211 | 212 | # self pretty-print 213 | self.indent(root) 214 | 215 | # wrap it in an ElementTree instance, and save as XML 216 | tree = ET.ElementTree(root) 217 | return tree 218 | 219 | def convertXMLToMetadata(self, tree): 220 | 221 | root = tree.getroot() 222 | 223 | if root.tag != 'ComicInfo': 224 | raise 1 225 | return None 226 | 227 | metadata = GenericMetadata() 228 | md = metadata 229 | 230 | # Helper function 231 | def xlate(tag): 232 | node = root.find(tag) 233 | if node is not None: 234 | return node.text 235 | else: 236 | return None 237 | 238 | md.series = xlate('Series') 239 | md.title = xlate('Title') 240 | md.issue = xlate('Number') 241 | md.issueCount = xlate('Count') 242 | md.volume = xlate('Volume') 243 | md.alternateSeries = xlate('AlternateSeries') 244 | md.alternateNumber = xlate('AlternateNumber') 245 | md.alternateCount = xlate('AlternateCount') 246 | md.comments = xlate('Summary') 247 | md.notes = xlate('Notes') 248 | md.year = xlate('Year') 249 | md.month = xlate('Month') 250 | md.day = xlate('Day') 251 | md.publisher = xlate('Publisher') 252 | md.imprint = xlate('Imprint') 253 | md.genre = xlate('Genre') 254 | md.webLink = xlate('Web') 255 | md.language = xlate('LanguageISO') 256 | md.format = xlate('Format') 257 | md.manga = xlate('Manga') 258 | md.characters = xlate('Characters') 259 | md.teams = xlate('Teams') 260 | md.locations = xlate('Locations') 261 | md.pageCount = xlate('PageCount') 262 | md.scanInfo = xlate('ScanInformation') 263 | md.storyArc = xlate('StoryArc') 264 | md.seriesGroup = xlate('SeriesGroup') 265 | md.maturityRating = xlate('AgeRating') 266 | md.gtin = xlate('GTIN') 267 | 268 | tmp = xlate('BlackAndWhite') 269 | md.blackAndWhite = False 270 | if tmp is not None and tmp.lower() in ["yes", "true", "1"]: 271 | md.blackAndWhite = True 272 | # Now extract the credit info 273 | for n in root: 274 | if (n.tag == 'Writer' or 275 | n.tag == 'Penciller' or 276 | n.tag == 'Inker' or 277 | n.tag == 'Colorist' or 278 | n.tag == 'Letterer' or 279 | n.tag == 'Editor'): 280 | if n.text is not None: 281 | for name in n.text.split(','): 282 | metadata.addCredit(name.strip(), n.tag) 283 | 284 | if n.tag == 'CoverArtist': 285 | if n.text is not None: 286 | for name in n.text.split(','): 287 | metadata.addCredit(name.strip(), "Cover") 288 | 289 | # Tags 290 | tags = xlate('Tags') 291 | if tags is not None: 292 | md.tags = [t for t in tags.split(", ")] 293 | 294 | # parse page data now 295 | pages_node = root.find("Pages") 296 | if pages_node is not None: 297 | for page in pages_node: 298 | metadata.pages.append(page.attrib) 299 | # print page.attrib 300 | 301 | metadata.isEmpty = False 302 | 303 | return metadata 304 | 305 | def writeToExternalFile(self, filename, metadata): 306 | 307 | tree = self.convertMetadataToXML(self, metadata) 308 | # ET.dump(tree) 309 | tree.write(filename, encoding='utf-8') 310 | 311 | def readFromExternalFile(self, filename): 312 | 313 | tree = ET.parse(filename) 314 | return self.convertXMLToMetadata(tree) 315 | 316 | 317 | def listToString(l): 318 | string = "" 319 | if l is not None: 320 | for item in l: 321 | if len(string) > 0: 322 | string += ", " 323 | string += item 324 | return string 325 | 326 | 327 | def tuple_to_string(metadata): 328 | if metadata and not (isinstance(metadata, str) or isinstance(metadata, unicode)): 329 | string = "" 330 | for item in metadata: 331 | if len(string) > 0: 332 | string += ", " 333 | string += item 334 | return string 335 | return metadata 336 | -------------------------------------------------------------------------------- /test-library/Unbekannt/zip comic (7)/metadata.opf: -------------------------------------------------------------------------------- 1 | <?xml version='1.0' encoding='utf-8'?> 2 | <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0"> 3 | <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> 4 | <dc:identifier opf:scheme="calibre" id="calibre_id">7</dc:identifier> 5 | <dc:identifier opf:scheme="uuid" id="uuid_id">a76943c0-eb0e-4292-b3f0-dbffa345603a</dc:identifier> 6 | <dc:title>zip comic</dc:title> 7 | <dc:creator opf:file-as="Unbekannt" opf:role="aut">Unbekannt</dc:creator> 8 | <dc:contributor opf:file-as="calibre" opf:role="bkp">calibre (8.7.0) [https://calibre-ebook.com]</dc:contributor> 9 | <dc:date>0101-01-01T00:00:00+00:00</dc:date> 10 | <dc:language>de</dc:language> 11 | <meta name="calibre:timestamp" content="2025-07-29T11:55:37.231145+00:00"/> 12 | <meta name="calibre:title_sort" content="zip comic"/> 13 | <meta name="calibre:user_metadata:#characters" content="{"table": "custom_column_2", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Characters", "search_terms": ["#characters"], "label": "characters", "colnum": 2, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 22, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 14 | <meta name="calibre:user_metadata:#colorist" content="{"table": "custom_column_14", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Colorist", "search_terms": ["#colorist"], "label": "colorist", "colnum": 14, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 23, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 15 | <meta name="calibre:user_metadata:#comicvine" content="{"table": "custom_column_11", "column": "value", "datatype": "comments", "is_multiple": null, "kind": "field", "name": "Comicvine", "search_terms": ["#comicvine"], "label": "comicvine", "colnum": 11, "display": {"heading_position": "hide", "interpret_as": "html", "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 24, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 16 | <meta name="calibre:user_metadata:#cover_artist" content="{"table": "custom_column_16", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Cover Artist", "search_terms": ["#cover_artist"], "label": "cover_artist", "colnum": 16, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 25, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 17 | <meta name="calibre:user_metadata:#editor" content="{"table": "custom_column_17", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Editor", "search_terms": ["#editor"], "label": "editor", "colnum": 17, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 26, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 18 | <meta name="calibre:user_metadata:#genre" content="{"table": "custom_column_5", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Genre", "search_terms": ["#genre"], "label": "genre", "colnum": 5, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 27, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 19 | <meta name="calibre:user_metadata:#image_size" content="{"table": "custom_column_9", "column": "value", "datatype": "float", "is_multiple": null, "kind": "field", "name": "Image size", "search_terms": ["#image_size"], "label": "image_size", "colnum": 9, "display": {"number_format": null, "decimals": 2, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 28, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 20 | <meta name="calibre:user_metadata:#inker" content="{"table": "custom_column_13", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Inker", "search_terms": ["#inker"], "label": "inker", "colnum": 13, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 29, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 21 | <meta name="calibre:user_metadata:#letterer" content="{"table": "custom_column_15", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Letterer", "search_terms": ["#letterer"], "label": "letterer", "colnum": 15, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 30, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 22 | <meta name="calibre:user_metadata:#locations" content="{"table": "custom_column_4", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Locations", "search_terms": ["#locations"], "label": "locations", "colnum": 4, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 31, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 23 | <meta name="calibre:user_metadata:#manga" content="{"table": "custom_column_7", "column": "value", "datatype": "enumeration", "is_multiple": null, "kind": "field", "name": "Manga", "search_terms": ["#manga"], "label": "manga", "colnum": 7, "display": {"enum_values": ["No", "Yes", "YesAndRightToLeft"], "enum_colors": [], "default_value": "No", "use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 32, "#value#": "No", "#extra#": null, "is_multiple2": {}}"/> 24 | <meta name="calibre:user_metadata:#number_issues" content="{"table": "custom_column_8", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Number of issues", "search_terms": ["#number_issues"], "label": "number_issues", "colnum": 8, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 33, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 25 | <meta name="calibre:user_metadata:#pages" content="{"table": "custom_column_12", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Pages", "search_terms": ["#pages"], "label": "pages", "colnum": 12, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 34, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 26 | <meta name="calibre:user_metadata:#penciller" content="{"table": "custom_column_1", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Penciller", "search_terms": ["#penciller"], "label": "penciller", "colnum": 1, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 35, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 27 | <meta name="calibre:user_metadata:#story_arc" content="{"table": "custom_column_10", "column": "value", "datatype": "series", "is_multiple": null, "kind": "field", "name": "Story Arc", "search_terms": ["#story_arc"], "label": "story_arc", "colnum": 10, "display": {"description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 36, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 28 | <meta name="calibre:user_metadata:#teams" content="{"table": "custom_column_3", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Teams", "search_terms": ["#teams"], "label": "teams", "colnum": 3, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 38, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 29 | <meta name="calibre:user_metadata:#volume" content="{"table": "custom_column_6", "column": "value", "datatype": "text", "is_multiple": null, "kind": "field", "name": "Volume", "search_terms": ["#volume"], "label": "volume", "colnum": 6, "display": {"use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 39, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 30 | </metadata> 31 | <guide> 32 | <reference type="cover" title="Titelbild" href="cover.jpg"/> 33 | </guide> 34 | </package> 35 | -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comicinfo (6)/metadata.opf: -------------------------------------------------------------------------------- 1 | <?xml version='1.0' encoding='utf-8'?> 2 | <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0"> 3 | <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> 4 | <dc:identifier opf:scheme="calibre" id="calibre_id">6</dc:identifier> 5 | <dc:identifier opf:scheme="uuid" id="uuid_id">b6ec71a3-b8dd-437e-a8d3-4750499c3caf</dc:identifier> 6 | <dc:title>Data only in comicinfo</dc:title> 7 | <dc:creator opf:file-as="Lastname, Firstname" opf:role="aut">Firstname Lastname</dc:creator> 8 | <dc:contributor opf:file-as="calibre" opf:role="bkp">calibre (8.7.0) [https://calibre-ebook.com]</dc:contributor> 9 | <dc:date>0101-01-01T00:00:00+00:00</dc:date> 10 | <dc:language>de</dc:language> 11 | <meta name="calibre:timestamp" content="2025-07-29T11:53:49.629377+00:00"/> 12 | <meta name="calibre:title_sort" content="Data only in comicinfo"/> 13 | <meta name="calibre:user_metadata:#characters" content="{"table": "custom_column_2", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Characters", "search_terms": ["#characters"], "label": "characters", "colnum": 2, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 22, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 14 | <meta name="calibre:user_metadata:#colorist" content="{"table": "custom_column_14", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Colorist", "search_terms": ["#colorist"], "label": "colorist", "colnum": 14, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 23, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 15 | <meta name="calibre:user_metadata:#comicvine" content="{"table": "custom_column_11", "column": "value", "datatype": "comments", "is_multiple": null, "kind": "field", "name": "Comicvine", "search_terms": ["#comicvine"], "label": "comicvine", "colnum": 11, "display": {"heading_position": "hide", "interpret_as": "html", "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 24, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 16 | <meta name="calibre:user_metadata:#cover_artist" content="{"table": "custom_column_16", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Cover Artist", "search_terms": ["#cover_artist"], "label": "cover_artist", "colnum": 16, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 25, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 17 | <meta name="calibre:user_metadata:#editor" content="{"table": "custom_column_17", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Editor", "search_terms": ["#editor"], "label": "editor", "colnum": 17, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 26, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 18 | <meta name="calibre:user_metadata:#genre" content="{"table": "custom_column_5", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Genre", "search_terms": ["#genre"], "label": "genre", "colnum": 5, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 27, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 19 | <meta name="calibre:user_metadata:#image_size" content="{"table": "custom_column_9", "column": "value", "datatype": "float", "is_multiple": null, "kind": "field", "name": "Image size", "search_terms": ["#image_size"], "label": "image_size", "colnum": 9, "display": {"number_format": null, "decimals": 2, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 28, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 20 | <meta name="calibre:user_metadata:#inker" content="{"table": "custom_column_13", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Inker", "search_terms": ["#inker"], "label": "inker", "colnum": 13, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 29, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 21 | <meta name="calibre:user_metadata:#letterer" content="{"table": "custom_column_15", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Letterer", "search_terms": ["#letterer"], "label": "letterer", "colnum": 15, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 30, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 22 | <meta name="calibre:user_metadata:#locations" content="{"table": "custom_column_4", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Locations", "search_terms": ["#locations"], "label": "locations", "colnum": 4, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 31, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 23 | <meta name="calibre:user_metadata:#manga" content="{"table": "custom_column_7", "column": "value", "datatype": "enumeration", "is_multiple": null, "kind": "field", "name": "Manga", "search_terms": ["#manga"], "label": "manga", "colnum": 7, "display": {"enum_values": ["No", "Yes", "YesAndRightToLeft"], "enum_colors": [], "default_value": "No", "use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 32, "#value#": "No", "#extra#": null, "is_multiple2": {}}"/> 24 | <meta name="calibre:user_metadata:#number_issues" content="{"table": "custom_column_8", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Number of issues", "search_terms": ["#number_issues"], "label": "number_issues", "colnum": 8, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 33, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 25 | <meta name="calibre:user_metadata:#pages" content="{"table": "custom_column_12", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Pages", "search_terms": ["#pages"], "label": "pages", "colnum": 12, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 34, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 26 | <meta name="calibre:user_metadata:#penciller" content="{"table": "custom_column_1", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Penciller", "search_terms": ["#penciller"], "label": "penciller", "colnum": 1, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 35, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 27 | <meta name="calibre:user_metadata:#story_arc" content="{"table": "custom_column_10", "column": "value", "datatype": "series", "is_multiple": null, "kind": "field", "name": "Story Arc", "search_terms": ["#story_arc"], "label": "story_arc", "colnum": 10, "display": {"description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 36, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 28 | <meta name="calibre:user_metadata:#teams" content="{"table": "custom_column_3", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Teams", "search_terms": ["#teams"], "label": "teams", "colnum": 3, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 38, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 29 | <meta name="calibre:user_metadata:#volume" content="{"table": "custom_column_6", "column": "value", "datatype": "text", "is_multiple": null, "kind": "field", "name": "Volume", "search_terms": ["#volume"], "label": "volume", "colnum": 6, "display": {"use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 39, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 30 | </metadata> 31 | <guide> 32 | <reference type="cover" title="Titelbild" href="cover.jpg"/> 33 | </guide> 34 | </package> 35 | -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data in both (5)/metadata.opf: -------------------------------------------------------------------------------- 1 | <?xml version='1.0' encoding='utf-8'?> 2 | <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0"> 3 | <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> 4 | <dc:identifier opf:scheme="calibre" id="calibre_id">5</dc:identifier> 5 | <dc:identifier opf:scheme="uuid" id="uuid_id">f2f15c83-8cdb-41ae-b10f-01b669ee7303</dc:identifier> 6 | <dc:title>Data in both</dc:title> 7 | <dc:creator opf:file-as="Lastname, Firstname" opf:role="aut">Firstname Lastname</dc:creator> 8 | <dc:contributor opf:file-as="calibre" opf:role="bkp">calibre (8.7.0) [https://calibre-ebook.com]</dc:contributor> 9 | <dc:date>2025-07-15T00:00:00+00:00</dc:date> 10 | <dc:publisher>BIG</dc:publisher> 11 | <dc:language>de</dc:language> 12 | <meta name="calibre:series" content="Awsome"/> 13 | <meta name="calibre:series_index" content="1"/> 14 | <meta name="calibre:timestamp" content="2025-07-29T11:53:21.264242+00:00"/> 15 | <meta name="calibre:title_sort" content="Data in both"/> 16 | <meta name="calibre:user_metadata:#characters" content="{"table": "custom_column_2", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Characters", "search_terms": ["#characters"], "label": "characters", "colnum": 2, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 22, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 17 | <meta name="calibre:user_metadata:#colorist" content="{"table": "custom_column_14", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Colorist", "search_terms": ["#colorist"], "label": "colorist", "colnum": 14, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 23, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 18 | <meta name="calibre:user_metadata:#comicvine" content="{"table": "custom_column_11", "column": "value", "datatype": "comments", "is_multiple": null, "kind": "field", "name": "Comicvine", "search_terms": ["#comicvine"], "label": "comicvine", "colnum": 11, "display": {"heading_position": "hide", "interpret_as": "html", "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 24, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 19 | <meta name="calibre:user_metadata:#cover_artist" content="{"table": "custom_column_16", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Cover Artist", "search_terms": ["#cover_artist"], "label": "cover_artist", "colnum": 16, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 25, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 20 | <meta name="calibre:user_metadata:#editor" content="{"table": "custom_column_17", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Editor", "search_terms": ["#editor"], "label": "editor", "colnum": 17, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 26, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 21 | <meta name="calibre:user_metadata:#genre" content="{"table": "custom_column_5", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Genre", "search_terms": ["#genre"], "label": "genre", "colnum": 5, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 27, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 22 | <meta name="calibre:user_metadata:#image_size" content="{"table": "custom_column_9", "column": "value", "datatype": "float", "is_multiple": null, "kind": "field", "name": "Image size", "search_terms": ["#image_size"], "label": "image_size", "colnum": 9, "display": {"number_format": null, "decimals": 2, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 28, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 23 | <meta name="calibre:user_metadata:#inker" content="{"table": "custom_column_13", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Inker", "search_terms": ["#inker"], "label": "inker", "colnum": 13, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 29, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 24 | <meta name="calibre:user_metadata:#letterer" content="{"table": "custom_column_15", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Letterer", "search_terms": ["#letterer"], "label": "letterer", "colnum": 15, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 30, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 25 | <meta name="calibre:user_metadata:#locations" content="{"table": "custom_column_4", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Locations", "search_terms": ["#locations"], "label": "locations", "colnum": 4, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 31, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 26 | <meta name="calibre:user_metadata:#manga" content="{"table": "custom_column_7", "column": "value", "datatype": "enumeration", "is_multiple": null, "kind": "field", "name": "Manga", "search_terms": ["#manga"], "label": "manga", "colnum": 7, "display": {"enum_values": ["No", "Yes", "YesAndRightToLeft"], "enum_colors": [], "default_value": "No", "use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 32, "#value#": "No", "#extra#": null, "is_multiple2": {}}"/> 27 | <meta name="calibre:user_metadata:#number_issues" content="{"table": "custom_column_8", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Number of issues", "search_terms": ["#number_issues"], "label": "number_issues", "colnum": 8, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 33, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 28 | <meta name="calibre:user_metadata:#pages" content="{"table": "custom_column_12", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Pages", "search_terms": ["#pages"], "label": "pages", "colnum": 12, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 34, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 29 | <meta name="calibre:user_metadata:#penciller" content="{"table": "custom_column_1", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Penciller", "search_terms": ["#penciller"], "label": "penciller", "colnum": 1, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 35, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 30 | <meta name="calibre:user_metadata:#story_arc" content="{"table": "custom_column_10", "column": "value", "datatype": "series", "is_multiple": null, "kind": "field", "name": "Story Arc", "search_terms": ["#story_arc"], "label": "story_arc", "colnum": 10, "display": {"description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 36, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 31 | <meta name="calibre:user_metadata:#teams" content="{"table": "custom_column_3", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Teams", "search_terms": ["#teams"], "label": "teams", "colnum": 3, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 38, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 32 | <meta name="calibre:user_metadata:#volume" content="{"table": "custom_column_6", "column": "value", "datatype": "text", "is_multiple": null, "kind": "field", "name": "Volume", "search_terms": ["#volume"], "label": "volume", "colnum": 6, "display": {"use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 39, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 33 | </metadata> 34 | <guide> 35 | <reference type="cover" title="Titelbild" href="cover.jpg"/> 36 | </guide> 37 | </package> 38 | -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in comment (4)/metadata.opf: -------------------------------------------------------------------------------- 1 | <?xml version='1.0' encoding='utf-8'?> 2 | <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0"> 3 | <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> 4 | <dc:identifier opf:scheme="calibre" id="calibre_id">4</dc:identifier> 5 | <dc:identifier opf:scheme="uuid" id="uuid_id">e6f4871a-9df8-4c43-9a24-1beec68abf9e</dc:identifier> 6 | <dc:title>Data only in comment</dc:title> 7 | <dc:creator opf:file-as="Lastname, Firstname" opf:role="aut">Firstname Lastname</dc:creator> 8 | <dc:contributor opf:file-as="calibre" opf:role="bkp">calibre (8.7.0) [https://calibre-ebook.com]</dc:contributor> 9 | <dc:date>2025-07-15T00:00:00+00:00</dc:date> 10 | <dc:publisher>BIG</dc:publisher> 11 | <dc:language>de</dc:language> 12 | <meta name="calibre:series" content="Awsome"/> 13 | <meta name="calibre:series_index" content="1"/> 14 | <meta name="calibre:timestamp" content="2025-07-29T11:52:51.874884+00:00"/> 15 | <meta name="calibre:title_sort" content="Data only in comment"/> 16 | <meta name="calibre:user_metadata:#characters" content="{"table": "custom_column_2", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Characters", "search_terms": ["#characters"], "label": "characters", "colnum": 2, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 22, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 17 | <meta name="calibre:user_metadata:#colorist" content="{"table": "custom_column_14", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Colorist", "search_terms": ["#colorist"], "label": "colorist", "colnum": 14, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 23, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 18 | <meta name="calibre:user_metadata:#comicvine" content="{"table": "custom_column_11", "column": "value", "datatype": "comments", "is_multiple": null, "kind": "field", "name": "Comicvine", "search_terms": ["#comicvine"], "label": "comicvine", "colnum": 11, "display": {"heading_position": "hide", "interpret_as": "html", "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 24, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 19 | <meta name="calibre:user_metadata:#cover_artist" content="{"table": "custom_column_16", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Cover Artist", "search_terms": ["#cover_artist"], "label": "cover_artist", "colnum": 16, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 25, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 20 | <meta name="calibre:user_metadata:#editor" content="{"table": "custom_column_17", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Editor", "search_terms": ["#editor"], "label": "editor", "colnum": 17, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 26, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 21 | <meta name="calibre:user_metadata:#genre" content="{"table": "custom_column_5", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Genre", "search_terms": ["#genre"], "label": "genre", "colnum": 5, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 27, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 22 | <meta name="calibre:user_metadata:#image_size" content="{"table": "custom_column_9", "column": "value", "datatype": "float", "is_multiple": null, "kind": "field", "name": "Image size", "search_terms": ["#image_size"], "label": "image_size", "colnum": 9, "display": {"number_format": null, "decimals": 2, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 28, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 23 | <meta name="calibre:user_metadata:#inker" content="{"table": "custom_column_13", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Inker", "search_terms": ["#inker"], "label": "inker", "colnum": 13, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 29, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 24 | <meta name="calibre:user_metadata:#letterer" content="{"table": "custom_column_15", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Letterer", "search_terms": ["#letterer"], "label": "letterer", "colnum": 15, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 30, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 25 | <meta name="calibre:user_metadata:#locations" content="{"table": "custom_column_4", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Locations", "search_terms": ["#locations"], "label": "locations", "colnum": 4, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 31, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 26 | <meta name="calibre:user_metadata:#manga" content="{"table": "custom_column_7", "column": "value", "datatype": "enumeration", "is_multiple": null, "kind": "field", "name": "Manga", "search_terms": ["#manga"], "label": "manga", "colnum": 7, "display": {"enum_values": ["No", "Yes", "YesAndRightToLeft"], "enum_colors": [], "default_value": "No", "use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 32, "#value#": "No", "#extra#": null, "is_multiple2": {}}"/> 27 | <meta name="calibre:user_metadata:#number_issues" content="{"table": "custom_column_8", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Number of issues", "search_terms": ["#number_issues"], "label": "number_issues", "colnum": 8, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 33, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 28 | <meta name="calibre:user_metadata:#pages" content="{"table": "custom_column_12", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Pages", "search_terms": ["#pages"], "label": "pages", "colnum": 12, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 34, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 29 | <meta name="calibre:user_metadata:#penciller" content="{"table": "custom_column_1", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Penciller", "search_terms": ["#penciller"], "label": "penciller", "colnum": 1, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 35, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 30 | <meta name="calibre:user_metadata:#story_arc" content="{"table": "custom_column_10", "column": "value", "datatype": "series", "is_multiple": null, "kind": "field", "name": "Story Arc", "search_terms": ["#story_arc"], "label": "story_arc", "colnum": 10, "display": {"description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 36, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 31 | <meta name="calibre:user_metadata:#teams" content="{"table": "custom_column_3", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Teams", "search_terms": ["#teams"], "label": "teams", "colnum": 3, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 38, "#value#": [], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 32 | <meta name="calibre:user_metadata:#volume" content="{"table": "custom_column_6", "column": "value", "datatype": "text", "is_multiple": null, "kind": "field", "name": "Volume", "search_terms": ["#volume"], "label": "volume", "colnum": 6, "display": {"use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 39, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 33 | </metadata> 34 | <guide> 35 | <reference type="cover" title="Titelbild" href="cover.jpg"/> 36 | </guide> 37 | </package> 38 | -------------------------------------------------------------------------------- /test-library/Firstname Lastname/Data only in calibre (2)/metadata.opf: -------------------------------------------------------------------------------- 1 | <?xml version='1.0' encoding='utf-8'?> 2 | <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0"> 3 | <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf"> 4 | <dc:identifier opf:scheme="calibre" id="calibre_id">2</dc:identifier> 5 | <dc:identifier opf:scheme="uuid" id="uuid_id">37306230-d62b-4edc-9428-8479a34e9453</dc:identifier> 6 | <dc:title>Data only in calibre</dc:title> 7 | <dc:creator opf:file-as="Lastname, Firstname" opf:role="aut">Firstname Lastname</dc:creator> 8 | <dc:contributor opf:file-as="calibre" opf:role="bkp">calibre (8.7.0) [https://calibre-ebook.com]</dc:contributor> 9 | <dc:date>2025-07-02T22:00:00+00:00</dc:date> 10 | <dc:publisher>BIG</dc:publisher> 11 | <dc:identifier opf:scheme="GTIN">123456789</dc:identifier> 12 | <dc:language>eng</dc:language> 13 | <meta name="calibre:series" content="Awsome"/> 14 | <meta name="calibre:series_index" content="1"/> 15 | <meta name="calibre:timestamp" content="2025-07-29T11:30:09+00:00"/> 16 | <meta name="calibre:title_sort" content="Data only in calibre"/> 17 | <meta name="calibre:user_metadata:#characters" content="{"table": "custom_column_2", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Characters", "search_terms": ["#characters"], "label": "characters", "colnum": 2, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 22, "#value#": ["Hero", "Villain"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 18 | <meta name="calibre:user_metadata:#colorist" content="{"table": "custom_column_14", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Colorist", "search_terms": ["#colorist"], "label": "colorist", "colnum": 14, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 23, "#value#": ["Colorist, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 19 | <meta name="calibre:user_metadata:#comicvine" content="{"table": "custom_column_11", "column": "value", "datatype": "comments", "is_multiple": null, "kind": "field", "name": "Comicvine", "search_terms": ["#comicvine"], "label": "comicvine", "colnum": 11, "display": {"heading_position": "hide", "interpret_as": "html", "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 24, "#value#": null, "#extra#": null, "is_multiple2": {}}"/> 20 | <meta name="calibre:user_metadata:#cover_artist" content="{"table": "custom_column_16", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Cover Artist", "search_terms": ["#cover_artist"], "label": "cover_artist", "colnum": 16, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 25, "#value#": ["Cover, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 21 | <meta name="calibre:user_metadata:#editor" content="{"table": "custom_column_17", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Editor", "search_terms": ["#editor"], "label": "editor", "colnum": 17, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 26, "#value#": ["Editor, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 22 | <meta name="calibre:user_metadata:#genre" content="{"table": "custom_column_5", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Genre", "search_terms": ["#genre"], "label": "genre", "colnum": 5, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 27, "#value#": ["Fantasy"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 23 | <meta name="calibre:user_metadata:#image_size" content="{"table": "custom_column_9", "column": "value", "datatype": "float", "is_multiple": null, "kind": "field", "name": "Image size", "search_terms": ["#image_size"], "label": "image_size", "colnum": 9, "display": {"number_format": null, "decimals": 2, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 28, "#value#": 2.1, "#extra#": null, "is_multiple2": {}}"/> 24 | <meta name="calibre:user_metadata:#inker" content="{"table": "custom_column_13", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Inker", "search_terms": ["#inker"], "label": "inker", "colnum": 13, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 29, "#value#": ["Inker, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 25 | <meta name="calibre:user_metadata:#letterer" content="{"table": "custom_column_15", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Letterer", "search_terms": ["#letterer"], "label": "letterer", "colnum": 15, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 30, "#value#": ["Letterer, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 26 | <meta name="calibre:user_metadata:#locations" content="{"table": "custom_column_4", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Locations", "search_terms": ["#locations"], "label": "locations", "colnum": 4, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 31, "#value#": ["Earth", "Moon"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 27 | <meta name="calibre:user_metadata:#manga" content="{"table": "custom_column_7", "column": "value", "datatype": "enumeration", "is_multiple": null, "kind": "field", "name": "Manga", "search_terms": ["#manga"], "label": "manga", "colnum": 7, "display": {"enum_values": ["No", "Yes", "YesAndRightToLeft"], "enum_colors": [], "default_value": "No", "use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 32, "#value#": "No", "#extra#": null, "is_multiple2": {}}"/> 28 | <meta name="calibre:user_metadata:#number_issues" content="{"table": "custom_column_8", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Number of issues", "search_terms": ["#number_issues"], "label": "number_issues", "colnum": 8, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 33, "#value#": 5, "#extra#": null, "is_multiple2": {}}"/> 29 | <meta name="calibre:user_metadata:#pages" content="{"table": "custom_column_12", "column": "value", "datatype": "int", "is_multiple": null, "kind": "field", "name": "Pages", "search_terms": ["#pages"], "label": "pages", "colnum": 12, "display": {"number_format": null, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": false, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 34, "#value#": 4, "#extra#": null, "is_multiple2": {}}"/> 30 | <meta name="calibre:user_metadata:#penciller" content="{"table": "custom_column_1", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Penciller", "search_terms": ["#penciller"], "label": "penciller", "colnum": 1, "display": {"is_names": true, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 35, "#value#": ["Colorist, Firstname"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": "&", "list_to_ui": " & "}}"/> 31 | <meta name="calibre:user_metadata:#story_arc" content="{"table": "custom_column_10", "column": "value", "datatype": "series", "is_multiple": null, "kind": "field", "name": "Story Arc", "search_terms": ["#story_arc"], "label": "story_arc", "colnum": 10, "display": {"description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 36, "#value#": "First Arc", "#extra#": 1.0, "is_multiple2": {}}"/> 32 | <meta name="calibre:user_metadata:#teams" content="{"table": "custom_column_3", "column": "value", "datatype": "text", "is_multiple": "|", "kind": "field", "name": "Teams", "search_terms": ["#teams"], "label": "teams", "colnum": 3, "display": {"is_names": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 38, "#value#": ["Heroes", "Villains"], "#extra#": null, "is_multiple2": {"cache_to_list": "|", "ui_to_list": ",", "list_to_ui": ", "}}"/> 33 | <meta name="calibre:user_metadata:#volume" content="{"table": "custom_column_6", "column": "value", "datatype": "text", "is_multiple": null, "kind": "field", "name": "Volume", "search_terms": ["#volume"], "label": "volume", "colnum": 6, "display": {"use_decorations": false, "description": "", "web_search_template": ""}, "is_custom": true, "is_category": true, "link_column": "value", "category_sort": "value", "is_csp": false, "is_editable": true, "rec_index": 39, "#value#": "1", "#extra#": null, "is_multiple2": {}}"/> 34 | </metadata> 35 | <guide> 36 | <reference type="cover" title="Cover" href="cover.jpg"/> 37 | </guide> 38 | </package> 39 | -------------------------------------------------------------------------------- /test-config/fonts/scanner_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "fonts": { 3 | "/opt/calibre/resources/fonts/liberation/LiberationMono-Bold.ttf||307996:1710386566.0": { 4 | "family_name": "Liberation Mono", 5 | "font-family": "Liberation Mono", 6 | "font-stretch": "normal", 7 | "font-style": "normal", 8 | "font-weight": "bold", 9 | "fs_type": 0, 10 | "full_name": "Liberation Mono Bold", 11 | "is_bold": true, 12 | "is_italic": false, 13 | "is_oblique": false, 14 | "is_otf": false, 15 | "is_regular": false, 16 | "is_wws": false, 17 | "os2_version": 3, 18 | "panose": [ 19 | 2, 20 | 7, 21 | 7, 22 | 9, 23 | 2, 24 | 2, 25 | 5, 26 | 2, 27 | 4, 28 | 4 29 | ], 30 | "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Bold.ttf", 31 | "preferred_family_name": null, 32 | "preferred_subfamily_name": null, 33 | "subfamily_name": "Bold", 34 | "weight": 700, 35 | "width": 5, 36 | "wws_family_name": null, 37 | "wws_subfamily_name": null 38 | }, 39 | "/opt/calibre/resources/fonts/liberation/LiberationMono-BoldItalic.ttf||284068:1710386566.0": { 40 | "family_name": "Liberation Mono", 41 | "font-family": "Liberation Mono", 42 | "font-stretch": "normal", 43 | "font-style": "italic", 44 | "font-weight": "bold", 45 | "fs_type": 0, 46 | "full_name": "Liberation Mono Bold Italic", 47 | "is_bold": true, 48 | "is_italic": true, 49 | "is_oblique": false, 50 | "is_otf": false, 51 | "is_regular": false, 52 | "is_wws": false, 53 | "os2_version": 3, 54 | "panose": [ 55 | 2, 56 | 7, 57 | 7, 58 | 9, 59 | 2, 60 | 2, 61 | 5, 62 | 9, 63 | 4, 64 | 4 65 | ], 66 | "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-BoldItalic.ttf", 67 | "preferred_family_name": null, 68 | "preferred_subfamily_name": null, 69 | "subfamily_name": "Bold Italic", 70 | "weight": 700, 71 | "width": 5, 72 | "wws_family_name": null, 73 | "wws_subfamily_name": null 74 | }, 75 | "/opt/calibre/resources/fonts/liberation/LiberationMono-Italic.ttf||281536:1710386566.0": { 76 | "family_name": "Liberation Mono", 77 | "font-family": "Liberation Mono", 78 | "font-stretch": "normal", 79 | "font-style": "italic", 80 | "font-weight": "normal", 81 | "fs_type": 0, 82 | "full_name": "Liberation Mono Italic", 83 | "is_bold": false, 84 | "is_italic": true, 85 | "is_oblique": false, 86 | "is_otf": false, 87 | "is_regular": false, 88 | "is_wws": false, 89 | "os2_version": 3, 90 | "panose": [ 91 | 2, 92 | 7, 93 | 4, 94 | 9, 95 | 2, 96 | 2, 97 | 5, 98 | 9, 99 | 4, 100 | 4 101 | ], 102 | "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Italic.ttf", 103 | "preferred_family_name": null, 104 | "preferred_subfamily_name": null, 105 | "subfamily_name": "Italic", 106 | "weight": 400, 107 | "width": 5, 108 | "wws_family_name": null, 109 | "wws_subfamily_name": null 110 | }, 111 | "/opt/calibre/resources/fonts/liberation/LiberationMono-Regular.ttf||319508:1710386566.0": { 112 | "family_name": "Liberation Mono", 113 | "font-family": "Liberation Mono", 114 | "font-stretch": "normal", 115 | "font-style": "normal", 116 | "font-weight": "normal", 117 | "fs_type": 0, 118 | "full_name": "Liberation Mono", 119 | "is_bold": false, 120 | "is_italic": false, 121 | "is_oblique": false, 122 | "is_otf": false, 123 | "is_regular": true, 124 | "is_wws": false, 125 | "os2_version": 3, 126 | "panose": [ 127 | 2, 128 | 7, 129 | 4, 130 | 9, 131 | 2, 132 | 2, 133 | 5, 134 | 2, 135 | 4, 136 | 4 137 | ], 138 | "path": "/opt/calibre/resources/fonts/liberation/LiberationMono-Regular.ttf", 139 | "preferred_family_name": null, 140 | "preferred_subfamily_name": null, 141 | "subfamily_name": "Regular", 142 | "weight": 400, 143 | "width": 5, 144 | "wws_family_name": null, 145 | "wws_subfamily_name": null 146 | }, 147 | "/opt/calibre/resources/fonts/liberation/LiberationSans-Bold.ttf||414456:1710386566.0": { 148 | "family_name": "Liberation Sans", 149 | "font-family": "Liberation Sans", 150 | "font-stretch": "normal", 151 | "font-style": "normal", 152 | "font-weight": "bold", 153 | "fs_type": 0, 154 | "full_name": "Liberation Sans Bold", 155 | "is_bold": true, 156 | "is_italic": false, 157 | "is_oblique": false, 158 | "is_otf": false, 159 | "is_regular": false, 160 | "is_wws": false, 161 | "os2_version": 3, 162 | "panose": [ 163 | 2, 164 | 11, 165 | 7, 166 | 4, 167 | 2, 168 | 2, 169 | 2, 170 | 2, 171 | 2, 172 | 4 173 | ], 174 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Bold.ttf", 175 | "preferred_family_name": null, 176 | "preferred_subfamily_name": null, 177 | "subfamily_name": "Bold", 178 | "weight": 700, 179 | "width": 5, 180 | "wws_family_name": null, 181 | "wws_subfamily_name": null 182 | }, 183 | "/opt/calibre/resources/fonts/liberation/LiberationSans-BoldItalic.ttf||408996:1710386566.0": { 184 | "family_name": "Liberation Sans", 185 | "font-family": "Liberation Sans", 186 | "font-stretch": "normal", 187 | "font-style": "italic", 188 | "font-weight": "bold", 189 | "fs_type": 0, 190 | "full_name": "Liberation Sans Bold Italic", 191 | "is_bold": true, 192 | "is_italic": true, 193 | "is_oblique": false, 194 | "is_otf": false, 195 | "is_regular": false, 196 | "is_wws": false, 197 | "os2_version": 3, 198 | "panose": [ 199 | 2, 200 | 11, 201 | 7, 202 | 4, 203 | 2, 204 | 2, 205 | 2, 206 | 9, 207 | 2, 208 | 4 209 | ], 210 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-BoldItalic.ttf", 211 | "preferred_family_name": null, 212 | "preferred_subfamily_name": null, 213 | "subfamily_name": "Bold Italic", 214 | "weight": 700, 215 | "width": 5, 216 | "wws_family_name": null, 217 | "wws_subfamily_name": null 218 | }, 219 | "/opt/calibre/resources/fonts/liberation/LiberationSans-Italic.ttf||415816:1710386566.0": { 220 | "family_name": "Liberation Sans", 221 | "font-family": "Liberation Sans", 222 | "font-stretch": "normal", 223 | "font-style": "italic", 224 | "font-weight": "normal", 225 | "fs_type": 0, 226 | "full_name": "Liberation Sans Italic", 227 | "is_bold": false, 228 | "is_italic": true, 229 | "is_oblique": false, 230 | "is_otf": false, 231 | "is_regular": false, 232 | "is_wws": false, 233 | "os2_version": 3, 234 | "panose": [ 235 | 2, 236 | 11, 237 | 6, 238 | 4, 239 | 2, 240 | 2, 241 | 2, 242 | 9, 243 | 2, 244 | 4 245 | ], 246 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Italic.ttf", 247 | "preferred_family_name": null, 248 | "preferred_subfamily_name": null, 249 | "subfamily_name": "Italic", 250 | "weight": 400, 251 | "width": 5, 252 | "wws_family_name": null, 253 | "wws_subfamily_name": null 254 | }, 255 | "/opt/calibre/resources/fonts/liberation/LiberationSans-Regular.ttf||410712:1710386566.0": { 256 | "family_name": "Liberation Sans", 257 | "font-family": "Liberation Sans", 258 | "font-stretch": "normal", 259 | "font-style": "normal", 260 | "font-weight": "normal", 261 | "fs_type": 0, 262 | "full_name": "Liberation Sans", 263 | "is_bold": false, 264 | "is_italic": false, 265 | "is_oblique": false, 266 | "is_otf": false, 267 | "is_regular": true, 268 | "is_wws": false, 269 | "os2_version": 3, 270 | "panose": [ 271 | 2, 272 | 11, 273 | 6, 274 | 4, 275 | 2, 276 | 2, 277 | 2, 278 | 2, 279 | 2, 280 | 4 281 | ], 282 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSans-Regular.ttf", 283 | "preferred_family_name": null, 284 | "preferred_subfamily_name": null, 285 | "subfamily_name": "Regular", 286 | "weight": 400, 287 | "width": 5, 288 | "wws_family_name": null, 289 | "wws_subfamily_name": null 290 | }, 291 | "/opt/calibre/resources/fonts/liberation/LiberationSerif-Bold.ttf||370096:1710386566.0": { 292 | "family_name": "Liberation Serif", 293 | "font-family": "Liberation Serif", 294 | "font-stretch": "normal", 295 | "font-style": "normal", 296 | "font-weight": "bold", 297 | "fs_type": 0, 298 | "full_name": "Liberation Serif Bold", 299 | "is_bold": true, 300 | "is_italic": false, 301 | "is_oblique": false, 302 | "is_otf": false, 303 | "is_regular": false, 304 | "is_wws": false, 305 | "os2_version": 3, 306 | "panose": [ 307 | 2, 308 | 2, 309 | 8, 310 | 3, 311 | 7, 312 | 5, 313 | 5, 314 | 2, 315 | 3, 316 | 4 317 | ], 318 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Bold.ttf", 319 | "preferred_family_name": null, 320 | "preferred_subfamily_name": null, 321 | "subfamily_name": "Bold", 322 | "weight": 700, 323 | "width": 5, 324 | "wws_family_name": null, 325 | "wws_subfamily_name": null 326 | }, 327 | "/opt/calibre/resources/fonts/liberation/LiberationSerif-BoldItalic.ttf||376772:1710386566.0": { 328 | "family_name": "Liberation Serif", 329 | "font-family": "Liberation Serif", 330 | "font-stretch": "normal", 331 | "font-style": "italic", 332 | "font-weight": "bold", 333 | "fs_type": 0, 334 | "full_name": "Liberation Serif Bold Italic", 335 | "is_bold": true, 336 | "is_italic": true, 337 | "is_oblique": false, 338 | "is_otf": false, 339 | "is_regular": false, 340 | "is_wws": false, 341 | "os2_version": 3, 342 | "panose": [ 343 | 2, 344 | 2, 345 | 7, 346 | 3, 347 | 6, 348 | 5, 349 | 5, 350 | 9, 351 | 3, 352 | 4 353 | ], 354 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-BoldItalic.ttf", 355 | "preferred_family_name": null, 356 | "preferred_subfamily_name": null, 357 | "subfamily_name": "Bold Italic", 358 | "weight": 700, 359 | "width": 5, 360 | "wws_family_name": null, 361 | "wws_subfamily_name": null 362 | }, 363 | "/opt/calibre/resources/fonts/liberation/LiberationSerif-Italic.ttf||375632:1710386566.0": { 364 | "family_name": "Liberation Serif", 365 | "font-family": "Liberation Serif", 366 | "font-stretch": "normal", 367 | "font-style": "italic", 368 | "font-weight": "normal", 369 | "fs_type": 0, 370 | "full_name": "Liberation Serif Italic", 371 | "is_bold": false, 372 | "is_italic": true, 373 | "is_oblique": false, 374 | "is_otf": false, 375 | "is_regular": false, 376 | "is_wws": false, 377 | "os2_version": 3, 378 | "panose": [ 379 | 2, 380 | 2, 381 | 5, 382 | 3, 383 | 5, 384 | 4, 385 | 5, 386 | 9, 387 | 3, 388 | 4 389 | ], 390 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Italic.ttf", 391 | "preferred_family_name": null, 392 | "preferred_subfamily_name": null, 393 | "subfamily_name": "Italic", 394 | "weight": 400, 395 | "width": 5, 396 | "wws_family_name": null, 397 | "wws_subfamily_name": null 398 | }, 399 | "/opt/calibre/resources/fonts/liberation/LiberationSerif-Regular.ttf||393576:1710386566.0": { 400 | "family_name": "Liberation Serif", 401 | "font-family": "Liberation Serif", 402 | "font-stretch": "normal", 403 | "font-style": "normal", 404 | "font-weight": "normal", 405 | "fs_type": 0, 406 | "full_name": "Liberation Serif", 407 | "is_bold": false, 408 | "is_italic": false, 409 | "is_oblique": false, 410 | "is_otf": false, 411 | "is_regular": true, 412 | "is_wws": false, 413 | "os2_version": 3, 414 | "panose": [ 415 | 2, 416 | 2, 417 | 6, 418 | 3, 419 | 5, 420 | 4, 421 | 5, 422 | 2, 423 | 3, 424 | 4 425 | ], 426 | "path": "/opt/calibre/resources/fonts/liberation/LiberationSerif-Regular.ttf", 427 | "preferred_family_name": null, 428 | "preferred_subfamily_name": null, 429 | "subfamily_name": "Regular", 430 | "weight": 400, 431 | "width": 5, 432 | "wws_family_name": null, 433 | "wws_subfamily_name": null 434 | }, 435 | "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf||708920:1691689145.0": { 436 | "family_name": "DejaVu Sans", 437 | "font-family": "DejaVu Sans", 438 | "font-stretch": "normal", 439 | "font-style": "normal", 440 | "font-weight": "bold", 441 | "fs_type": 0, 442 | "full_name": "DejaVu Sans Bold", 443 | "is_bold": true, 444 | "is_italic": false, 445 | "is_oblique": false, 446 | "is_otf": false, 447 | "is_regular": false, 448 | "is_wws": false, 449 | "os2_version": 1, 450 | "panose": [ 451 | 2, 452 | 11, 453 | 8, 454 | 3, 455 | 3, 456 | 6, 457 | 4, 458 | 2, 459 | 2, 460 | 4 461 | ], 462 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 463 | "preferred_family_name": "DejaVu Sans", 464 | "preferred_subfamily_name": "Bold", 465 | "subfamily_name": "Bold", 466 | "weight": 700, 467 | "width": 5, 468 | "wws_family_name": null, 469 | "wws_subfamily_name": null 470 | }, 471 | "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf||759720:1691689145.0": { 472 | "family_name": "DejaVu Sans", 473 | "font-family": "DejaVu Sans", 474 | "font-stretch": "normal", 475 | "font-style": "normal", 476 | "font-weight": "normal", 477 | "fs_type": 0, 478 | "full_name": "DejaVu Sans", 479 | "is_bold": false, 480 | "is_italic": false, 481 | "is_oblique": false, 482 | "is_otf": false, 483 | "is_regular": true, 484 | "is_wws": false, 485 | "os2_version": 1, 486 | "panose": [ 487 | 2, 488 | 11, 489 | 6, 490 | 3, 491 | 3, 492 | 8, 493 | 4, 494 | 2, 495 | 2, 496 | 4 497 | ], 498 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 499 | "preferred_family_name": "DejaVu Sans", 500 | "preferred_subfamily_name": "Book", 501 | "subfamily_name": "Book", 502 | "weight": 400, 503 | "width": 5, 504 | "wws_family_name": null, 505 | "wws_subfamily_name": null 506 | }, 507 | "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf||334268:1691689145.0": { 508 | "family_name": "DejaVu Sans Mono", 509 | "font-family": "DejaVu Sans Mono", 510 | "font-stretch": "normal", 511 | "font-style": "normal", 512 | "font-weight": "bold", 513 | "fs_type": 0, 514 | "full_name": "DejaVu Sans Mono Bold", 515 | "is_bold": true, 516 | "is_italic": false, 517 | "is_oblique": false, 518 | "is_otf": false, 519 | "is_regular": false, 520 | "is_wws": false, 521 | "os2_version": 1, 522 | "panose": [ 523 | 2, 524 | 11, 525 | 7, 526 | 9, 527 | 3, 528 | 6, 529 | 4, 530 | 2, 531 | 2, 532 | 4 533 | ], 534 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf", 535 | "preferred_family_name": null, 536 | "preferred_subfamily_name": null, 537 | "subfamily_name": "Bold", 538 | "weight": 700, 539 | "width": 5, 540 | "wws_family_name": null, 541 | "wws_subfamily_name": null 542 | }, 543 | "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf||254960:1691689145.0": { 544 | "family_name": "DejaVu Sans Mono", 545 | "font-family": "DejaVu Sans Mono", 546 | "font-stretch": "normal", 547 | "font-style": "italic", 548 | "font-weight": "bold", 549 | "fs_type": 0, 550 | "full_name": "DejaVu Sans Mono Bold Oblique", 551 | "is_bold": true, 552 | "is_italic": true, 553 | "is_oblique": false, 554 | "is_otf": false, 555 | "is_regular": false, 556 | "is_wws": false, 557 | "os2_version": 1, 558 | "panose": [ 559 | 2, 560 | 11, 561 | 7, 562 | 9, 563 | 3, 564 | 3, 565 | 4, 566 | 11, 567 | 2, 568 | 4 569 | ], 570 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-BoldOblique.ttf", 571 | "preferred_family_name": null, 572 | "preferred_subfamily_name": null, 573 | "subfamily_name": "Bold Oblique", 574 | "weight": 700, 575 | "width": 5, 576 | "wws_family_name": null, 577 | "wws_subfamily_name": null 578 | }, 579 | "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf||253448:1691689145.0": { 580 | "family_name": "DejaVu Sans Mono", 581 | "font-family": "DejaVu Sans Mono", 582 | "font-stretch": "normal", 583 | "font-style": "italic", 584 | "font-weight": "normal", 585 | "fs_type": 0, 586 | "full_name": "DejaVu Sans Mono Oblique", 587 | "is_bold": false, 588 | "is_italic": true, 589 | "is_oblique": false, 590 | "is_otf": false, 591 | "is_regular": false, 592 | "is_wws": false, 593 | "os2_version": 1, 594 | "panose": [ 595 | 2, 596 | 11, 597 | 6, 598 | 9, 599 | 3, 600 | 3, 601 | 4, 602 | 11, 603 | 2, 604 | 4 605 | ], 606 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Oblique.ttf", 607 | "preferred_family_name": null, 608 | "preferred_subfamily_name": null, 609 | "subfamily_name": "Oblique", 610 | "weight": 400, 611 | "width": 5, 612 | "wws_family_name": null, 613 | "wws_subfamily_name": null 614 | }, 615 | "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf||343140:1691689145.0": { 616 | "family_name": "DejaVu Sans Mono", 617 | "font-family": "DejaVu Sans Mono", 618 | "font-stretch": "normal", 619 | "font-style": "normal", 620 | "font-weight": "normal", 621 | "fs_type": 0, 622 | "full_name": "DejaVu Sans Mono", 623 | "is_bold": false, 624 | "is_italic": false, 625 | "is_oblique": false, 626 | "is_otf": false, 627 | "is_regular": true, 628 | "is_wws": false, 629 | "os2_version": 1, 630 | "panose": [ 631 | 2, 632 | 11, 633 | 6, 634 | 9, 635 | 3, 636 | 8, 637 | 4, 638 | 2, 639 | 2, 640 | 4 641 | ], 642 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 643 | "preferred_family_name": null, 644 | "preferred_subfamily_name": null, 645 | "subfamily_name": "Book", 646 | "weight": 400, 647 | "width": 5, 648 | "wws_family_name": null, 649 | "wws_subfamily_name": null 650 | }, 651 | "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf||356668:1691689145.0": { 652 | "family_name": "DejaVu Serif", 653 | "font-family": "DejaVu Serif", 654 | "font-stretch": "normal", 655 | "font-style": "normal", 656 | "font-weight": "bold", 657 | "fs_type": 0, 658 | "full_name": "DejaVu Serif Bold", 659 | "is_bold": true, 660 | "is_italic": false, 661 | "is_oblique": false, 662 | "is_otf": false, 663 | "is_regular": false, 664 | "is_wws": false, 665 | "os2_version": 1, 666 | "panose": [ 667 | 2, 668 | 6, 669 | 8, 670 | 3, 671 | 5, 672 | 6, 673 | 5, 674 | 2, 675 | 2, 676 | 4 677 | ], 678 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf", 679 | "preferred_family_name": "DejaVu Serif", 680 | "preferred_subfamily_name": "Bold", 681 | "subfamily_name": "Bold", 682 | "weight": 700, 683 | "width": 5, 684 | "wws_family_name": null, 685 | "wws_subfamily_name": null 686 | }, 687 | "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf||380660:1691689145.0": { 688 | "family_name": "DejaVu Serif", 689 | "font-family": "DejaVu Serif", 690 | "font-stretch": "normal", 691 | "font-style": "normal", 692 | "font-weight": "normal", 693 | "fs_type": 0, 694 | "full_name": "DejaVu Serif", 695 | "is_bold": false, 696 | "is_italic": false, 697 | "is_oblique": false, 698 | "is_otf": false, 699 | "is_regular": true, 700 | "is_wws": false, 701 | "os2_version": 1, 702 | "panose": [ 703 | 2, 704 | 6, 705 | 6, 706 | 3, 707 | 5, 708 | 6, 709 | 5, 710 | 2, 711 | 2, 712 | 4 713 | ], 714 | "path": "/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf", 715 | "preferred_family_name": "DejaVu Serif", 716 | "preferred_subfamily_name": "Book", 717 | "subfamily_name": "Book", 718 | "weight": 400, 719 | "width": 5, 720 | "wws_family_name": null, 721 | "wws_subfamily_name": null 722 | } 723 | }, 724 | "version": 2 725 | } --------------------------------------------------------------------------------