├── 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[^_]+)",
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 = '\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 |
2 |
3 |
4 | 7
5 | a76943c0-eb0e-4292-b3f0-dbffa345603a
6 | zip comic
7 | Unbekannt
8 | calibre (8.7.0) [https://calibre-ebook.com]
9 | 0101-01-01T00:00:00+00:00
10 | de
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/test-library/Firstname Lastname/Data only in comicinfo (6)/metadata.opf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 6
5 | b6ec71a3-b8dd-437e-a8d3-4750499c3caf
6 | Data only in comicinfo
7 | Firstname Lastname
8 | calibre (8.7.0) [https://calibre-ebook.com]
9 | 0101-01-01T00:00:00+00:00
10 | de
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/test-library/Firstname Lastname/Data in both (5)/metadata.opf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 5
5 | f2f15c83-8cdb-41ae-b10f-01b669ee7303
6 | Data in both
7 | Firstname Lastname
8 | calibre (8.7.0) [https://calibre-ebook.com]
9 | 2025-07-15T00:00:00+00:00
10 | BIG
11 | de
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test-library/Firstname Lastname/Data only in comment (4)/metadata.opf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 4
5 | e6f4871a-9df8-4c43-9a24-1beec68abf9e
6 | Data only in comment
7 | Firstname Lastname
8 | calibre (8.7.0) [https://calibre-ebook.com]
9 | 2025-07-15T00:00:00+00:00
10 | BIG
11 | de
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/test-library/Firstname Lastname/Data only in calibre (2)/metadata.opf:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 2
5 | 37306230-d62b-4edc-9428-8479a34e9453
6 | Data only in calibre
7 | Firstname Lastname
8 | calibre (8.7.0) [https://calibre-ebook.com]
9 | 2025-07-02T22:00:00+00:00
10 | BIG
11 | 123456789
12 | eng
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
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 | }
--------------------------------------------------------------------------------