[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