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 <[email protected]> + + * Added importer for OnMyShelf JSON files. + 2025-08-30 Robby Stephenson <[email protected]> * 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 <[email protected]> + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 <[email protected]> + Copyright (C) 2025 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -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 <[email protected]> + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 <[email protected]> + Copyright (C) 2025 Robby Stephenson <[email protected]> ***************************************************************************/ /*************************************************************************** @@ -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 {
