[office/tellico] /: Add importer for OnMyShelf JSON files
Robby Stephenson
null at kde.org
Sun Aug 31 20:14:18 BST 2025
Git commit 165369f892fdf1ff3df455b2e70f914852bfb930 by Robby Stephenson.
Committed on 31/08/2025 at 19:14.
Pushed by rstephenson into branch 'master'.
Add importer for OnMyShelf JSON files
M +4 -0 ChangeLog
M +1 -0 doc/importing-exporting.docbook
M +1 -2 icons/CMakeLists.txt
M +1 -0 icons/icons.qrc
A +- -- icons/onmyshelf.png
M +15 -2 src/importdialog.cpp
M +4 -0 src/mainwindow.cpp
M +2 -1 src/tellicoui.rc
M +6 -0 src/tests/CMakeLists.txt
A +301 -0 src/tests/data/onmyshelf-boardgames.json
A +619 -0 src/tests/data/onmyshelf-books.json
A +235 -0 src/tests/data/onmyshelf-comics.json
A +231 -0 src/tests/data/onmyshelf-movies.json
A +159 -0 src/tests/onmyshelftest.cpp [License: GPL (v2/3)]
C +13 -70 src/tests/onmyshelftest.h [from: src/translators/translators.h - 061% similarity]
M +1 -0 src/translators/CMakeLists.txt
A +240 -0 src/translators/onmyshelfimporter.cpp [License: GPL (v2/3)]
C +31 -66 src/translators/onmyshelfimporter.h [from: src/translators/translators.h - 062% similarity]
M +2 -1 src/translators/translators.h
https://invent.kde.org/office/tellico/-/commit/165369f892fdf1ff3df455b2e70f914852bfb930
diff --git a/ChangeLog b/ChangeLog
index b8e3a76ec..01b29ed7b 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,7 @@
+2025-08-31 Robby Stephenson <robby at periapsis.org>
+
+ * Added importer for OnMyShelf JSON files.
+
2025-08-30 Robby Stephenson <robby at periapsis.org>
* Added KDE shortcut keys for previous/next tab (Bug 508789).
diff --git a/doc/importing-exporting.docbook b/doc/importing-exporting.docbook
index 53d38899b..10d27fc89 100644
--- a/doc/importing-exporting.docbook
+++ b/doc/importing-exporting.docbook
@@ -51,6 +51,7 @@ To facilitate the use of barcode scanners, searches can include multiple ISBN/UP
<para>&appname; can import data directly from a variety of other collection management programs, including
<application><ulink url="https://gitlab.com/GCstar/GCstar">GCstar</ulink></application>,
+ <application><ulink url="https://onmyshelf.app">OnMyShelf</ulink></application>,
<application><ulink url="https://www.datacrow.net/">Data Crow</ulink></application>,
<application><ulink url="https://github.com/mvz/alexandria-book-collection-manager">Alexandria</ulink></application>,
<application><ulink url="https://www.delicious-monster.com">Delicious Library</ulink></application>,
diff --git a/icons/CMakeLists.txt b/icons/CMakeLists.txt
index febe5d141..bc540c073 100644
--- a/icons/CMakeLists.txt
+++ b/icons/CMakeLists.txt
@@ -30,6 +30,7 @@ set(PIC_FILES
nocover_comic.png
nocover_game.png
nocover_video.png
+ onmyshelf.png
person-open.png
person.png
README.icons
@@ -83,5 +84,3 @@ ecm_install_icons(ICONS ${ICON_FILES}
DESTINATION ${KDE_INSTALL_ICONDIR}
THEME hicolor
)
-
-
diff --git a/icons/icons.qrc b/icons/icons.qrc
index e8d6cf221..612163353 100644
--- a/icons/icons.qrc
+++ b/icons/icons.qrc
@@ -29,6 +29,7 @@
<file alias="nocover_comic.png">nocover_comic.png</file>
<file alias="nocover_game.png">nocover_game.png</file>
<file alias="nocover_video.png">nocover_video.png</file>
+ <file alias="onmyshelf.png">onmyshelf.png</file>
<file alias="person-open.png">person-open.png</file>
<file alias="person.png">person.png</file>
<file alias="referencer.png">referencer.png</file>
diff --git a/icons/onmyshelf.png b/icons/onmyshelf.png
new file mode 100644
index 000000000..89e5c41e2
Binary files /dev/null and b/icons/onmyshelf.png differ
diff --git a/src/importdialog.cpp b/src/importdialog.cpp
index e9b0cd826..f098b4c26 100644
--- a/src/importdialog.cpp
+++ b/src/importdialog.cpp
@@ -57,6 +57,7 @@
#include "translators/marcimporter.h"
#include "translators/ebookimporter.h"
#include "translators/discogsimporter.h"
+#include "translators/onmyshelfimporter.h"
#include "utils/datafileregistry.h"
#include <KLocalizedString>
@@ -185,8 +186,11 @@ Tellico::Import::Action ImportDialog::action() const {
// static
Tellico::Import::Importer* ImportDialog::importer(Tellico::Import::Format format_, const QList<QUrl>& urls_) {
-#define CHECK_SIZE if(urls_.size() > 1) myWarning() << "only importing first URL"
- QUrl firstURL = urls_.isEmpty() ? QUrl() : urls_[0];
+ const QUrl firstURL = urls_.isEmpty() ? QUrl() : urls_[0];
+#define CHECK_SIZE \
+ if(urls_.size() > 1) myWarning() << "Only importing first URL"; \
+ else myLog() << "Importing" << firstURL.toDisplayString(QUrl::PreferLocalFile);
+
Import::Importer* importer = nullptr;
switch(format_) {
case Import::TellicoXML:
@@ -334,6 +338,11 @@ Tellico::Import::Importer* ImportDialog::importer(Tellico::Import::Format format
CHECK_SIZE;
importer = new Import::DiscogsImporter();
break;
+
+ case Import::OnMyShelf:
+ CHECK_SIZE;
+ importer = new Import::OnMyShelfImporter(firstURL);
+ break;
}
if(!importer) {
myWarning() << "importer not created!";
@@ -425,6 +434,10 @@ QString ImportDialog::fileFilter(Tellico::Import::Format format_) {
text = i18n("eBook Files") + QLatin1String(" (*.epub *.fb2 *.fb2zip *.mobi)") + QLatin1String(";;");
break;
+ case Import::OnMyShelf:
+ text = i18n("JSON Files") + QLatin1String(" (*.json)") + QLatin1String(";;");
+ break;
+
case Import::AudioFile:
case Import::Alexandria:
case Import::FreeDB:
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 186a988c5..197f90207 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -390,6 +390,10 @@ void MainWindow::initActions() {
i18n("Import data from Data Crow"),
QIcon::fromTheme(QStringLiteral("datacrow"), QIcon(QLatin1String(":/icons/datacrow"))));
+ IMPORT_ACTION(Import::OnMyShelf, "file_import_onmyshelf", i18n("Import OnMyShelf Data..."),
+ i18n("Import data from OnMyShelf"),
+ QIcon::fromTheme(QStringLiteral("onmyshelf"), QIcon(QLatin1String(":/icons/onmyshelf"))));
+
IMPORT_ACTION(Import::Referencer, "file_import_referencer", i18n("Import Referencer Data..."),
i18n("Import data from Referencer"),
QIcon::fromTheme(QStringLiteral("referencer"), QIcon(QLatin1String(":/icons/referencer"))));
diff --git a/src/tellicoui.rc b/src/tellicoui.rc
index 9aefbc759..b72f4f4de 100644
--- a/src/tellicoui.rc
+++ b/src/tellicoui.rc
@@ -1,6 +1,6 @@
<?xml version = '1.0'?>
<!DOCTYPE kpartgui SYSTEM "kpartgui.dtd">
-<kpartgui version="49" name="tellico">
+<kpartgui version="50" name="tellico">
<MenuBar>
<Menu name="file">
<text>&File</text>
@@ -31,6 +31,7 @@
<Action name="file_import_datacrow"/>
<Action name="file_import_delicious"/>
<Action name="file_import_gcstar"/>
+ <Action name="file_import_onmyshelf"/>
<Separator/>
<Action name="file_import_alexandria"/>
<Action name="file_import_bibtex"/>
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 98e5d62e6..f9b6496d9 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -422,6 +422,12 @@ ecm_add_test(modstest.cpp
LINK_LIBRARIES ${TELLICO_TEST_LIBS} translatorstest
)
+ecm_add_test(onmyshelftest.cpp
+ ../translators/onmyshelfimporter.cpp
+ TEST_NAME onmyshelftest
+ LINK_LIBRARIES ${TELLICO_TEST_LIBS} translatorstest
+)
+
ecm_add_test(referencertest.cpp
../translators/referencerimporter.cpp
TEST_NAME referencertest
diff --git a/src/tests/data/onmyshelf-boardgames.json b/src/tests/data/onmyshelf-boardgames.json
new file mode 100644
index 000000000..0b6e9a13f
--- /dev/null
+++ b/src/tests/data/onmyshelf-boardgames.json
@@ -0,0 +1,301 @@
+{
+ "id": 8,
+ "name": {
+ "en_US": "My board games"
+ },
+ "description": [],
+ "cover": null,
+ "thumbnails": [],
+ "owner": 1,
+ "type": "board_games",
+ "visibility": 3,
+ "borrowable": 3,
+ "created": "2025-08-31 18:21:18",
+ "updated": "2025-08-31 18:21:18",
+ "properties": {
+ "image": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 1,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Image"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "name": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 1,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Name",
+ "fr_FR": "Nom"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "age": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Age"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "author": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Author",
+ "fr_FR": "Auteur"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "my author"
+ ]
+ },
+ "editor": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Editor",
+ "fr_FR": "Éditeur"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "my editor"
+ ]
+ },
+ "illustrator": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Illustrateur"
+ },
+ "description": {
+ "fr_FR": null
+ },
+ "values": [
+ "my illustrator"
+ ]
+ },
+ "images": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 1,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Images"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "language": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Language",
+ "fr_FR": "Langue"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "english"
+ ]
+ },
+ "mechanism": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Mécanisme"
+ },
+ "description": {
+ "fr_FR": null
+ },
+ "values": [
+ "mechanism"
+ ]
+ }
+ },
+ "tags": [
+ "board_games"
+ ],
+ "items": [
+ {
+ "id": 12375,
+ "collectionId": 8,
+ "properties": {
+ "age": "7-99",
+ "author": "my author",
+ "editor": "my editor",
+ "illustrator": "my illustrator",
+ "language": "english",
+ "mechanism": "mechanism",
+ "name": "name"
+ },
+ "quantity": 1,
+ "visibility": 0,
+ "borrowable": 3,
+ "created": "2025-08-31 18:21:44",
+ "updated": "2025-08-31 18:21:44",
+ "lent": false,
+ "pendingLoans": 0,
+ "askingLoans": 0,
+ "loans": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/tests/data/onmyshelf-books.json b/src/tests/data/onmyshelf-books.json
new file mode 100644
index 000000000..92aae362c
--- /dev/null
+++ b/src/tests/data/onmyshelf-books.json
@@ -0,0 +1,619 @@
+{
+ "id": 6,
+ "name": {
+ "en_US": "My books"
+ },
+ "description": [],
+ "cover": null,
+ "thumbnails": [],
+ "owner": 1,
+ "type": "books",
+ "visibility": 3,
+ "borrowable": 3,
+ "created": "2025-08-30 19:45:56",
+ "updated": "2025-08-30 19:45:56",
+ "properties": {
+ "cover": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 1,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Cover",
+ "fr_FR": "Couverture"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "title": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 1,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Title",
+ "fr_FR": "Titre"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "subtitle": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 1,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Subtitle",
+ "fr_FR": "Sous-titre"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "author": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 1,
+ "filterable": 1,
+ "searchable": 1,
+ "sortable": 0,
+ "order": 10,
+ "hidden": 0,
+ "label": {
+ "en_US": "Author",
+ "fr_FR": "Auteur"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "my author1",
+ "my author2"
+ ]
+ },
+ "series": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 1,
+ "sortable": 0,
+ "order": 8,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Série"
+ },
+ "description": {
+ "fr_FR": null
+ },
+ "values": [
+ "series"
+ ]
+ },
+ "pub_year": {
+ "type": "number",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 1,
+ "order": 7,
+ "hidden": 0,
+ "label": {
+ "en_US": "Published year",
+ "fr_FR": "Année de publication"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "series_number": {
+ "type": "number",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 7,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Tome"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "genre": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 6,
+ "hidden": 0,
+ "label": {
+ "en_US": "Genre"
+ },
+ "description": {
+ "en_US": null
+ },
+ "values": [
+ "genre"
+ ]
+ },
+ "editor": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 9,
+ "hidden": 0,
+ "label": {
+ "en_US": "Editor",
+ "fr_FR": "Éditeur"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "my editor"
+ ]
+ },
+ "format": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 5,
+ "hidden": 0,
+ "label": {
+ "en_US": "Format"
+ },
+ "description": {
+ "en_US": null
+ },
+ "values": [
+ "Paperback"
+ ]
+ },
+ "pages": {
+ "type": "number",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 1,
+ "order": 4,
+ "hidden": 0,
+ "label": {
+ "en_US": "Pages"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "publisher": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 3,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Maison d'édition"
+ },
+ "description": {
+ "fr_FR": null
+ },
+ "values": [
+ "publisher"
+ ]
+ },
+ "summary": {
+ "type": "longtext",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 2,
+ "hidden": 0,
+ "label": {
+ "en_US": "Summary",
+ "fr_FR": "Résumé"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "language": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 1,
+ "hidden": 0,
+ "label": {
+ "en_US": "Language",
+ "fr_FR": "Langue"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ },
+ "values": [
+ "English"
+ ]
+ },
+ "images": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 1,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Images"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "isbn": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "ISBN"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "read": {
+ "type": "yesno",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Lu"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "signed": {
+ "type": "yesno",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 3,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Signé"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "source": {
+ "type": "url",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 3,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 1,
+ "label": {
+ "en_US": "Source"
+ },
+ "description": {
+ "en_US": null
+ }
+ }
+ },
+ "tags": [
+ "books"
+ ],
+ "items": [
+ {
+ "id": 12372,
+ "collectionId": 6,
+ "properties": {
+ "author": [
+ "my author1",
+ "my author2"
+ ],
+ "editor": "my editor",
+ "format": "Paperback",
+ "genre": "genre",
+ "isbn": "0671578499",
+ "language": "English",
+ "pages": "101",
+ "publisher": "publisher",
+ "pub_year": "1999",
+ "read": "1",
+ "series": "series",
+ "series_number": "1",
+ "subtitle": "subtitle",
+ "summary": "summary of thigns\n\nnext line",
+ "title": "title"
+ },
+ "quantity": 1,
+ "visibility": 0,
+ "borrowable": 3,
+ "created": "2025-08-30 19:47:01",
+ "updated": "2025-08-30 19:47:01",
+ "lent": true,
+ "pendingLoans": 0,
+ "askingLoans": 0,
+ "loans": [
+ {
+ "id": 1,
+ "itemId": 12372,
+ "borrowerId": 1,
+ "state": "lent",
+ "lent": 1756604340,
+ "returned": null,
+ "notes": "loaned this time",
+ "date": "2025-08-31 01:39:26",
+ "borrower": "my borrower",
+ "email": null
+ }
+ ]
+ },
+ {
+ "id": 12373,
+ "collectionId": 6,
+ "properties": {
+ "author": "my author1",
+ "series": "series",
+ "series_number": "2",
+ "title": "title2"
+ },
+ "quantity": 1,
+ "visibility": 0,
+ "borrowable": 3,
+ "created": "2025-08-30 19:47:28",
+ "updated": "2025-08-30 19:47:28",
+ "lent": false,
+ "pendingLoans": 0,
+ "askingLoans": 0,
+ "loans": []
+ }
+ ]
+}
diff --git a/src/tests/data/onmyshelf-comics.json b/src/tests/data/onmyshelf-comics.json
new file mode 100644
index 000000000..aa1713f6b
--- /dev/null
+++ b/src/tests/data/onmyshelf-comics.json
@@ -0,0 +1,235 @@
+{
+ "id": 9,
+ "name": {
+ "en_US": "My comics"
+ },
+ "description": [],
+ "cover": null,
+ "thumbnails": [],
+ "owner": 1,
+ "type": "comics",
+ "visibility": 3,
+ "borrowable": 3,
+ "created": "2025-08-31 18:21:52",
+ "updated": "2025-08-31 18:21:52",
+ "properties": {
+ "cover": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 1,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Cover",
+ "fr_FR": "Couverture"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "title": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 1,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Title",
+ "fr_FR": "Titre"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "series": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 1,
+ "sortable": 0,
+ "order": 8,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Série"
+ },
+ "description": {
+ "fr_FR": null
+ },
+ "values": [
+ "series"
+ ]
+ },
+ "series_number": {
+ "type": "number",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 1,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 7,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Tome"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "format": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Format"
+ },
+ "description": {
+ "en_US": null
+ },
+ "values": [
+ "Paperback"
+ ]
+ },
+ "synopsis": {
+ "type": "longtext",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Synopsis"
+ },
+ "description": {
+ "en_US": null
+ }
+ },
+ "writer": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Auteur"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ }
+ },
+ "tags": [
+ "comics"
+ ],
+ "items": [
+ {
+ "id": 12376,
+ "collectionId": 9,
+ "properties": {
+ "format": "Paperback",
+ "series": "series",
+ "series_number": "1",
+ "synopsis": "synopsis",
+ "title": "title",
+ "writer": "my author"
+ },
+ "quantity": 1,
+ "visibility": 0,
+ "borrowable": 3,
+ "created": "2025-08-31 18:22:21",
+ "updated": "2025-08-31 18:22:21",
+ "lent": false,
+ "pendingLoans": 0,
+ "askingLoans": 0,
+ "loans": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/tests/data/onmyshelf-movies.json b/src/tests/data/onmyshelf-movies.json
new file mode 100644
index 000000000..a8807cbef
--- /dev/null
+++ b/src/tests/data/onmyshelf-movies.json
@@ -0,0 +1,231 @@
+{
+ "id": 7,
+ "name": {
+ "en_US": "My movies"
+ },
+ "description": [],
+ "cover": null,
+ "thumbnails": [],
+ "owner": 1,
+ "type": "movies",
+ "visibility": 3,
+ "borrowable": 3,
+ "created": "2025-08-31 18:20:02",
+ "updated": "2025-08-31 18:20:02",
+ "properties": {
+ "cover": {
+ "type": "image",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 1,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Cover",
+ "fr_FR": "Couverture"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "title": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 1,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Title",
+ "fr_FR": "Titre"
+ },
+ "description": {
+ "en_US": null,
+ "fr_FR": null
+ }
+ },
+ "director": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Réalisateur"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "format": {
+ "type": "text",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 1,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "en_US": "Format"
+ },
+ "description": {
+ "en_US": null
+ },
+ "values": [
+ "widescreen?"
+ ]
+ },
+ "rating": {
+ "type": "rating",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 1,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Note"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "synopsis": {
+ "type": "longtext",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Résumé"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ },
+ "trailer": {
+ "type": "video",
+ "suffix": null,
+ "default": null,
+ "authorizedValues": null,
+ "visibility": 0,
+ "required": 0,
+ "hideLabel": 0,
+ "isId": 0,
+ "isTitle": 0,
+ "isSubTitle": 0,
+ "isCover": 0,
+ "preview": 0,
+ "multiple": 0,
+ "filterable": 0,
+ "searchable": 0,
+ "sortable": 0,
+ "order": 0,
+ "hidden": 0,
+ "label": {
+ "fr_FR": "Bande-annonce"
+ },
+ "description": {
+ "fr_FR": null
+ }
+ }
+ },
+ "tags": [
+ "movies"
+ ],
+ "items": [
+ {
+ "id": 12374,
+ "collectionId": 7,
+ "properties": {
+ "director": "my director",
+ "format": "widescreen?",
+ "rating": "1",
+ "synopsis": "plot here",
+ "title": "title"
+ },
+ "quantity": 1,
+ "visibility": 0,
+ "borrowable": 3,
+ "created": "2025-08-31 18:20:53",
+ "updated": "2025-08-31 18:20:53",
+ "lent": false,
+ "pendingLoans": 0,
+ "askingLoans": 0,
+ "loans": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/tests/onmyshelftest.cpp b/src/tests/onmyshelftest.cpp
new file mode 100644
index 000000000..62c107515
--- /dev/null
+++ b/src/tests/onmyshelftest.cpp
@@ -0,0 +1,159 @@
+/***************************************************************************
+ Copyright (C) 2025 Robby Stephenson <robby at periapsis.org>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ * *
+ ***************************************************************************/
+
+#undef QT_NO_CAST_FROM_ASCII
+
+#include "onmyshelftest.h"
+
+#include "../translators/onmyshelfimporter.h"
+#include "../collections/bookcollection.h"
+#include "../collections/videocollection.h"
+#include "../collections/musiccollection.h"
+#include "../collectionfactory.h"
+#include "../fieldformat.h"
+
+#include <KLocalizedString>
+
+#include <QTest>
+#include <QStandardPaths>
+
+QTEST_GUILESS_MAIN( OnMyShelfTest )
+
+void OnMyShelfTest::initTestCase() {
+ QStandardPaths::setTestModeEnabled(true);
+ KLocalizedString::setApplicationDomain("tellico");
+ // need to register the collection type
+ Tellico::RegisterCollection<Tellico::Data::BookCollection> registerBook(Tellico::Data::Collection::Book, "book");
+ Tellico::RegisterCollection<Tellico::Data::VideoCollection> registerVideo(Tellico::Data::Collection::Video, "video");
+ Tellico::RegisterCollection<Tellico::Data::MusicCollection> registerMusic(Tellico::Data::Collection::Album, "album");
+}
+
+void OnMyShelfTest::testBooks() {
+ QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/onmyshelf-books.json"));
+ Tellico::Import::OnMyShelfImporter importer(url);
+ QVERIFY(importer.canImport(Tellico::Data::Collection::Book));
+ Tellico::Data::CollPtr coll = importer.collection();
+
+ QVERIFY(coll);
+ QCOMPARE(coll->type(), Tellico::Data::Collection::Book);
+ QCOMPARE(coll->entryCount(), 2);
+ QCOMPARE(coll->title(), QStringLiteral("My books"));
+
+ Tellico::Data::EntryPtr entry = coll->entryById(1);
+ QVERIFY(entry);
+ QCOMPARE(entry->field("title"), QStringLiteral("title"));
+ QCOMPARE(entry->field("pub_year"), QStringLiteral("1999"));
+ QCOMPARE(entry->field("author"), QStringLiteral("my author1; my author2"));
+ QCOMPARE(entry->field("publisher"), QStringLiteral("publisher"));
+ QCOMPARE(entry->field("isbn"), QStringLiteral("0-671-57849-9"));
+ QCOMPARE(entry->field("binding"), QStringLiteral("Paperback"));
+ QCOMPARE(entry->field("series"), QStringLiteral("series"));
+ QCOMPARE(entry->field("series_num"), QStringLiteral("1"));
+ QCOMPARE(entry->field("read"), QStringLiteral("true"));
+ QCOMPARE(entry->field("genre"), QStringLiteral("genre"));
+ QCOMPARE(entry->field("cdate"), QStringLiteral("2025-08-30"));
+ QCOMPARE(entry->field("mdate"), QStringLiteral("2025-08-30"));
+ QVERIFY(!entry->field("plot").isEmpty());
+ QVERIFY(entry->field("plot").contains(QLatin1String("<br/>"))); // \n is converted
+
+ const auto borrowers = coll->borrowers();
+ QCOMPARE(borrowers.count(), 1);
+ const auto borrower = borrowers.at(0);
+ QVERIFY(borrower);
+ QCOMPARE(borrower->name(), QStringLiteral("my borrower"));
+
+ const auto loans = borrower->loans();
+ QCOMPARE(loans.count(), 1);
+ const auto loan = loans.at(0);
+ QVERIFY(loan);
+ QCOMPARE(loan->loanDate(), QDate::fromString("2025-08-31", Qt::ISODate));
+ QCOMPARE(loan->note(), QStringLiteral("loaned this time"));
+ QVERIFY(loan->entry());
+ QCOMPARE(loan->entry()->id(), entry->id());
+}
+
+void OnMyShelfTest::testMovies() {
+ QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/onmyshelf-movies.json"));
+ Tellico::Import::OnMyShelfImporter importer(url);
+ QVERIFY(importer.canImport(Tellico::Data::Collection::Video));
+ Tellico::Data::CollPtr coll = importer.collection();
+
+ QVERIFY(coll);
+ QCOMPARE(coll->type(), Tellico::Data::Collection::Video);
+ QCOMPARE(coll->entryCount(), 1);
+ QCOMPARE(coll->title(), QStringLiteral("My movies"));
+
+ Tellico::Data::EntryPtr entry = coll->entryById(1);
+ QVERIFY(entry);
+ QCOMPARE(entry->field("title"), QStringLiteral("title"));
+ QCOMPARE(entry->field("director"), QStringLiteral("my director"));
+ QCOMPARE(entry->field("rating"), QStringLiteral("1"));
+ // not sure what format should be
+ QCOMPARE(entry->field("cdate"), QStringLiteral("2025-08-31"));
+ QCOMPARE(entry->field("mdate"), QStringLiteral("2025-08-31"));
+ QCOMPARE(entry->field("plot"), QStringLiteral("plot here"));
+}
+
+void OnMyShelfTest::testComics() {
+ QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/onmyshelf-comics.json"));
+ Tellico::Import::OnMyShelfImporter importer(url);
+ QVERIFY(importer.canImport(Tellico::Data::Collection::ComicBook));
+ Tellico::Data::CollPtr coll = importer.collection();
+
+ QVERIFY(coll);
+ QCOMPARE(coll->type(), Tellico::Data::Collection::ComicBook);
+ QCOMPARE(coll->entryCount(), 1);
+ QCOMPARE(coll->title(), QStringLiteral("My comics"));
+
+ Tellico::Data::EntryPtr entry = coll->entryById(1);
+ QVERIFY(entry);
+ QCOMPARE(entry->field("title"), QStringLiteral("title"));
+ QCOMPARE(entry->field("writer"), QStringLiteral("my author"));
+ QCOMPARE(entry->field("series"), QStringLiteral("series"));
+ QCOMPARE(entry->field("issue"), QStringLiteral("1"));
+ // not sure about format
+ QCOMPARE(entry->field("cdate"), QStringLiteral("2025-08-31"));
+ QCOMPARE(entry->field("mdate"), QStringLiteral("2025-08-31"));
+ QCOMPARE(entry->field("plot"), QStringLiteral("synopsis"));
+}
+
+void OnMyShelfTest::testBoardGames() {
+ QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/onmyshelf-boardgames.json"));
+ Tellico::Import::OnMyShelfImporter importer(url);
+ QVERIFY(importer.canImport(Tellico::Data::Collection::BoardGame));
+ Tellico::Data::CollPtr coll = importer.collection();
+
+ QVERIFY(coll);
+ QCOMPARE(coll->type(), Tellico::Data::Collection::BoardGame);
+ QCOMPARE(coll->entryCount(), 1);
+ QCOMPARE(coll->title(), QStringLiteral("My board games"));
+
+ Tellico::Data::EntryPtr entry = coll->entryById(1);
+ QVERIFY(entry);
+ QCOMPARE(entry->field("title"), QStringLiteral("name"));
+ QCOMPARE(entry->field("mechanism"), QStringLiteral("mechanism"));
+ QCOMPARE(entry->field("designer"), QStringLiteral("my author"));
+ QCOMPARE(entry->field("minimum-age"), QStringLiteral("7"));
+ // skip editor, illustrator, language
+}
diff --git a/src/translators/translators.h b/src/tests/onmyshelftest.h
similarity index 61%
copy from src/translators/translators.h
copy to src/tests/onmyshelftest.h
index 1e0edf9cd..762ad5a66 100644
--- a/src/translators/translators.h
+++ b/src/tests/onmyshelftest.h
@@ -1,5 +1,5 @@
/***************************************************************************
- Copyright (C) 2003-2009 Robby Stephenson <robby at periapsis.org>
+ Copyright (C) 2025 Robby Stephenson <robby at periapsis.org>
***************************************************************************/
/***************************************************************************
@@ -22,77 +22,20 @@
* *
***************************************************************************/
-#ifndef TRANSLATORS_H
-#define TRANSLATORS_H
+#ifndef ONMYSHELFTEST_H
+#define ONMYSHELFTEST_H
-namespace Tellico {
- namespace Import {
- enum Format {
- TellicoXML = 0,
- Bibtex,
- Bibtexml,
- CSV,
- XSLT,
- AudioFile,
- MODS,
- Alexandria,
- FreeDB,
- RIS,
- GCstar,
- FileListing,
- GRS1,
- AMC,
- Griffith,
- PDF,
- Referencer,
- Delicious,
- Goodreads,
- CIW,
- VinoXML,
- BoardGameGeek,
- LibraryThing,
- Collectorz,
- DataCrow,
- MARC,
- EBook,
- Discogs
- };
+#include <QObject>
- enum Action {
- Replace,
- Append,
- Merge
- };
+class OnMyShelfTest : public QObject {
+Q_OBJECT
- enum Target {
- None,
- File,
- Dir
- };
- }
-
- namespace Export {
- enum Format {
- TellicoXML = 0,
- TellicoZip,
- Bibtex,
- Bibtexml,
- HTML,
- CSV,
- XSLT,
- Text,
- PilotDB, // Deprecated
- Alexandria,
- ONIX,
- GCstar
- };
-
- enum Target {
- None,
- File,
- Dir
- };
- }
-}
+private Q_SLOTS:
+ void initTestCase();
+ void testBooks();
+ void testMovies();
+ void testComics();
+ void testBoardGames();
+};
#endif
diff --git a/src/translators/CMakeLists.txt b/src/translators/CMakeLists.txt
index 402c3e178..1d61d5c5a 100644
--- a/src/translators/CMakeLists.txt
+++ b/src/translators/CMakeLists.txt
@@ -38,6 +38,7 @@ set(translators_STAT_SRCS
librarythingimporter.cpp
marcimporter.cpp
onixexporter.cpp
+ onmyshelfimporter.cpp
pdfimporter.cpp
referencerimporter.cpp
risimporter.cpp
diff --git a/src/translators/onmyshelfimporter.cpp b/src/translators/onmyshelfimporter.cpp
new file mode 100644
index 000000000..d0759d2e5
--- /dev/null
+++ b/src/translators/onmyshelfimporter.cpp
@@ -0,0 +1,240 @@
+/***************************************************************************
+ Copyright (C) 2025 Robby Stephenson <robby at periapsis.org>
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or *
+ * modify it under the terms of the GNU General Public License as *
+ * published by the Free Software Foundation; either version 2 of *
+ * the License or (at your option) version 3 or any later version *
+ * accepted by the membership of KDE e.V. (or its successor approved *
+ * by the membership of KDE e.V.), which shall act as a proxy *
+ * defined in Section 14 of version 3 of the license. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see <http://www.gnu.org/licenses/>. *
+ * *
+ ***************************************************************************/
+
+#include "onmyshelfimporter.h"
+#include "../collections/bookcollection.h"
+#include "../collections/videocollection.h"
+#include "../collections/comicbookcollection.h"
+#include "../collections/boardgamecollection.h"
+#include "../core/filehandler.h"
+#include "../utils/objvalue.h"
+#include "../utils/isbnvalidator.h"
+#include "../tellico_debug.h"
+
+#include <KLocalizedString>
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonParseError>
+
+using Tellico::Import::OnMyShelfImporter;
+using namespace Qt::Literals::StringLiterals;
+
+OnMyShelfImporter::OnMyShelfImporter(const QUrl& url) : Import::Importer(url) {
+}
+
+bool OnMyShelfImporter::canImport(int type) const {
+ return type == Data::Collection::Book ||
+ type == Data::Collection::Video ||
+ type == Data::Collection::ComicBook ||
+ type == Data::Collection::BoardGame;
+}
+
+Tellico::Data::CollPtr OnMyShelfImporter::collection() {
+ if(m_coll) {
+ return m_coll;
+ }
+
+ const QByteArray data = Tellico::FileHandler::readDataFile(url(), false /* quiet */);
+ QJsonParseError parseError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
+ if(doc.isNull()) {
+ myDebug() << "Bad json data:" << parseError.errorString();
+ return Data::CollPtr();
+ }
+
+ const auto topObj = doc.object();
+
+ const auto collType = topObj["type"_L1].toString();
+ myLog() << "Reading collection type:" << collType;
+ if(collType == "books"_L1) {
+ m_coll = new Data::BookCollection(true);
+ } else if(collType == "movies"_L1) {
+ m_coll = new Data::VideoCollection(true);
+ } else if(collType == "comics"_L1) {
+ m_coll = new Data::ComicBookCollection(true);
+ } else if(collType == "board_games"_L1) {
+ m_coll = new Data::BoardGameCollection(true);
+ }
+ if(!m_coll) {
+ myLog() << "No collection created";
+ return Data::CollPtr();
+ }
+
+ // choose language key from first value in collection names
+ QString langKey;
+ const auto collNameObj = topObj["name"_L1].toObject();
+ for(auto i = collNameObj.constBegin(); i != collNameObj.constEnd(); ++i) {
+ langKey = i.key();
+ m_coll->setTitle(i.value().toString());
+ break;
+ }
+ if(langKey.isEmpty()) {
+ langKey = "en_US"_L1;
+ }
+ myLog() << "Using language key:" << langKey;
+
+ Data::EntryList entries;
+ QHash<int, Data::BorrowerPtr> borrowerHash;
+
+ const auto items = topObj["items"_L1].toArray();
+ for(auto i = items.constBegin(); i != items.constEnd(); ++i) {
+ const auto itemObj = i->toObject();
+ const auto propObj = itemObj["properties"_L1].toObject();
+
+ Data::EntryPtr entry(new Data::Entry(m_coll));
+
+ switch(m_coll->type()) {
+ case Data::Collection::Book:
+ populateBooks(entry, propObj);
+ break;
+ case Data::Collection::Video:
+ populateMovies(entry, propObj);
+ break;
+ case Data::Collection::ComicBook:
+ populateComics(entry, propObj);
+ break;
+ case Data::Collection::BoardGame:
+ populateBoardGames(entry, propObj);
+ break;
+ default:
+ break;
+ }
+
+ // cut off time portion of the datestamp
+ entry->setField(QStringLiteral("cdate"), objValue(itemObj, "created").left(10), false);
+ entry->setField(QStringLiteral("mdate"), objValue(itemObj, "updated").left(10), false);
+
+ const auto loansArray = itemObj["loans"_L1].toArray();
+ if(!loansArray.isEmpty() && loansArray.at(0)["state"_L1].toString() == "lent"_L1) {
+ const auto loanObj = loansArray.at(0).toObject();
+ Data::BorrowerPtr b;
+ const auto bId = loanObj["borrowerId"_L1].toInt();
+ if(borrowerHash.contains(bId)) {
+ b = borrowerHash[bId];
+ } else {
+ b = Data::BorrowerPtr(new Data::Borrower(objValue(loanObj, "borrower"), QString()));
+ borrowerHash.insert(bId, b);
+ m_coll->addBorrower(b);
+ }
+ const auto loanDate = objValue(loanObj, "date").left(10);
+ Data::LoanPtr l(new Data::Loan(entry, QDate::fromString(loanDate, Qt::ISODate), QDate(), QString()));
+ l->setNote(objValue(loanObj, "notes"));
+ b->addLoan(l);
+ }
+
+ entries += entry;
+ }
+ m_coll->addEntries(entries);
+ return m_coll;
+}
+
+void OnMyShelfImporter::populateBooks(Data::EntryPtr entry_, const QJsonObject& obj_) {
+ const bool updateModified = false;
+
+ entry_->setField("title"_L1, objValue(obj_, "title"), updateModified);
+ entry_->setField("subtitle"_L1, objValue(obj_, "subtitle"), updateModified);
+ entry_->setField("author"_L1, objValue(obj_, "author"), updateModified);
+ entry_->setField("editor"_L1, objValue(obj_, "editor"), updateModified);
+ entry_->setField("publisher"_L1, objValue(obj_, "publisher"), updateModified);
+ entry_->setField("pub_year"_L1, objValue(obj_, "pub_year"), updateModified);
+ entry_->setField("genre"_L1, objValue(obj_, "genre"), updateModified);
+ entry_->setField("series"_L1, objValue(obj_, "series"), updateModified);
+ entry_->setField("series_num"_L1, objValue(obj_, "series_number"), updateModified);
+ entry_->setField("language"_L1, objValue(obj_, "language"), updateModified);
+
+ auto summary = objValue(obj_, "summary");
+ summary.replace('\n'_L1, "<br/>"_L1);
+ entry_->setField("plot"_L1, summary, updateModified);
+
+ QString isbn = objValue(obj_, "isbn");
+ ISBNValidator::staticFixup(isbn);
+ entry_->setField("isbn"_L1, isbn, false);
+
+ // grab first set of digits
+ static const QRegularExpression digits(QStringLiteral("\\d+"));
+ auto match = digits.match(objValue(obj_, "pages"));
+ if(match.hasMatch()) {
+ entry_->setField(QStringLiteral("pages"), match.captured(0), updateModified);
+ }
+
+ const auto read = objValue(obj_, "read");
+ if(read == "1"_L1 || read == "true"_L1 || read == "yes"_L1) {
+ entry_->setField(QStringLiteral("read"), "true"_L1, updateModified);
+ }
+
+ const auto format = objValue(obj_, "format");
+ if(!format.isEmpty()) {
+ const QString bindingName(QStringLiteral("binding"));
+ if(format == QLatin1String("Paperback")) {
+ entry_->setField(bindingName, i18n("Paperback"), updateModified);
+ } else if(format == QLatin1String("Hardcover")) {
+ entry_->setField(bindingName, i18n("Hardback"), updateModified);
+ } else {
+ // just in case there's a value there
+ entry_->setField(bindingName, format, updateModified);
+ }
+ }
+}
+
+void OnMyShelfImporter::populateMovies(Data::EntryPtr entry_, const QJsonObject& obj_) {
+ const bool updateModified = false;
+
+ entry_->setField("title"_L1, objValue(obj_, "title"), updateModified);
+ entry_->setField("director"_L1, objValue(obj_, "director"), updateModified);
+ entry_->setField("rating"_L1, objValue(obj_, "rating"), updateModified);
+
+ auto summary = objValue(obj_, "synopsis");
+ summary.replace('\n'_L1, "<br/>"_L1);
+ entry_->setField("plot"_L1, summary, updateModified);
+}
+
+void OnMyShelfImporter::populateComics(Data::EntryPtr entry_, const QJsonObject& obj_) {
+ const bool updateModified = false;
+
+ entry_->setField("title"_L1, objValue(obj_, "title"), updateModified);
+ entry_->setField("writer"_L1, objValue(obj_, "writer"), updateModified);
+ entry_->setField("series"_L1, objValue(obj_, "series"), updateModified);
+ entry_->setField("issue"_L1, objValue(obj_, "series_number"), updateModified);
+
+ auto summary = objValue(obj_, "synopsis");
+ summary.replace('\n'_L1, "<br/>"_L1);
+ entry_->setField("plot"_L1, summary, updateModified);
+}
+
+void OnMyShelfImporter::populateBoardGames(Data::EntryPtr entry_, const QJsonObject& obj_) {
+ const bool updateModified = false;
+
+ entry_->setField("title"_L1, objValue(obj_, "name"), updateModified);
+ entry_->setField("designer"_L1, objValue(obj_, "author"), updateModified);
+ entry_->setField("mechanism"_L1, objValue(obj_, "mechanism"), updateModified);
+
+ // grab first set of digits at beginning
+ static const QRegularExpression digits(QStringLiteral("^(\\d+)-?"));
+ auto match = digits.match(objValue(obj_, "age"));
+ if(match.hasMatch()) {
+ entry_->setField(QStringLiteral("minimum-age"), match.captured(1), updateModified);
+ }
+}
diff --git a/src/translators/translators.h b/src/translators/onmyshelfimporter.h
similarity index 62%
copy from src/translators/translators.h
copy to src/translators/onmyshelfimporter.h
index 1e0edf9cd..3eaef4779 100644
--- a/src/translators/translators.h
+++ b/src/translators/onmyshelfimporter.h
@@ -1,5 +1,5 @@
/***************************************************************************
- Copyright (C) 2003-2009 Robby Stephenson <robby at periapsis.org>
+ Copyright (C) 2025 Robby Stephenson <robby at periapsis.org>
***************************************************************************/
/***************************************************************************
@@ -22,77 +22,42 @@
* *
***************************************************************************/
-#ifndef TRANSLATORS_H
-#define TRANSLATORS_H
+#ifndef TELLICO_IMPORT_ONMYSHELFIMPORTER_H
+#define TELLICO_IMPORT_ONMYSHELFIMPORTER_H
+
+#include "importer.h"
namespace Tellico {
namespace Import {
- enum Format {
- TellicoXML = 0,
- Bibtex,
- Bibtexml,
- CSV,
- XSLT,
- AudioFile,
- MODS,
- Alexandria,
- FreeDB,
- RIS,
- GCstar,
- FileListing,
- GRS1,
- AMC,
- Griffith,
- PDF,
- Referencer,
- Delicious,
- Goodreads,
- CIW,
- VinoXML,
- BoardGameGeek,
- LibraryThing,
- Collectorz,
- DataCrow,
- MARC,
- EBook,
- Discogs
- };
- enum Action {
- Replace,
- Append,
- Merge
- };
+/**
+ * @author Robby Stephenson
+*/
+class OnMyShelfImporter : public Importer {
+Q_OBJECT
+
+public:
+ /**
+ */
+ OnMyShelfImporter(const QUrl& url);
+
+ virtual Data::CollPtr collection() override;
+ virtual bool canImport(int type) const override;
+
+ virtual QWidget* widget(QWidget*) override { return nullptr; }
- enum Target {
- None,
- File,
- Dir
- };
- }
+public Q_SLOTS:
+ void slotCancel() override {}
- namespace Export {
- enum Format {
- TellicoXML = 0,
- TellicoZip,
- Bibtex,
- Bibtexml,
- HTML,
- CSV,
- XSLT,
- Text,
- PilotDB, // Deprecated
- Alexandria,
- ONIX,
- GCstar
- };
+private:
+ void populateBooks(Data::EntryPtr entry, const QJsonObject& obj);
+ void populateMovies(Data::EntryPtr entry, const QJsonObject& obj);
+ void populateComics(Data::EntryPtr entry, const QJsonObject& obj);
+ void populateBoardGames(Data::EntryPtr entry, const QJsonObject& obj);
- enum Target {
- None,
- File,
- Dir
- };
- }
-}
+ Data::CollPtr m_coll;
+};
+ } // end namespace
+} // end namespace
#endif
diff --git a/src/translators/translators.h b/src/translators/translators.h
index 1e0edf9cd..c5a8b09ef 100644
--- a/src/translators/translators.h
+++ b/src/translators/translators.h
@@ -55,7 +55,8 @@ namespace Tellico {
DataCrow,
MARC,
EBook,
- Discogs
+ Discogs,
+ OnMyShelf
};
enum Action {
More information about the kde-doc-english
mailing list