From b4828512694ffccdd8d53ccff6c8c5a4486090cf Mon Sep 17 00:00:00 2001 From: Alek Rudnik Date: Mon, 30 Aug 2021 23:15:29 +0200 Subject: [PATCH] [EGD-6836] Add multimedia indexer database Added multimedia indexer database. Decided to got with single table approach. User playlist will be implemented as seprate tables when needed. --- image/user/db/multimedia_001.sql | 21 ++ module-db/CMakeLists.txt | 10 +- module-db/Databases/MultimediaFilesDB.cpp | 7 + module-db/Databases/MultimediaFilesDB.hpp | 15 ++ module-db/Tables/MultimediaFilesTable.cpp | 188 ++++++++++++++++++ module-db/Tables/MultimediaFilesTable.hpp | 78 ++++++++ module-db/tests/CMakeLists.txt | 1 + .../tests/MultimediaFilesTable_tests.cpp | 149 ++++++++++++++ 8 files changed, 465 insertions(+), 4 deletions(-) create mode 100644 image/user/db/multimedia_001.sql create mode 100644 module-db/Databases/MultimediaFilesDB.cpp create mode 100644 module-db/Databases/MultimediaFilesDB.hpp create mode 100644 module-db/Tables/MultimediaFilesTable.cpp create mode 100644 module-db/Tables/MultimediaFilesTable.hpp create mode 100644 module-db/tests/MultimediaFilesTable_tests.cpp diff --git a/image/user/db/multimedia_001.sql b/image/user/db/multimedia_001.sql new file mode 100644 index 000000000..bcacfc7ff --- /dev/null +++ b/image/user/db/multimedia_001.sql @@ -0,0 +1,21 @@ +-- Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +CREATE TABLE IF NOT EXISTS files +( + _id INTEGER PRIMARY KEY, + path TEXT, + media_type TEXT, + size INTEGER, + title TEXT, + artist TEXT, + album TEXT, + comment TEXT, + genre TEXT, + year INTEGER, + track INTEGER, + song_length INTEGER, + bitrate INTEGER, + sample_rate INTEGER, + channels INTEGER +); diff --git a/module-db/CMakeLists.txt b/module-db/CMakeLists.txt index 8401e6478..7bebd704e 100644 --- a/module-db/CMakeLists.txt +++ b/module-db/CMakeLists.txt @@ -17,14 +17,15 @@ set(SOURCES Database/sqlite3vfs.cpp ${SQLITE3_SOURCE} - Databases/ContactsDB.cpp - Databases/EventsDB.cpp - Databases/SmsDB.cpp Databases/AlarmsDB.cpp - Databases/NotesDB.cpp Databases/CalllogDB.cpp + Databases/ContactsDB.cpp Databases/CountryCodesDB.cpp + Databases/EventsDB.cpp + Databases/MultimediaFilesDB.cpp + Databases/NotesDB.cpp Databases/NotificationsDB.cpp + Databases/SmsDB.cpp Tables/AlarmEventsTable.cpp Tables/Table.cpp @@ -43,6 +44,7 @@ set(SOURCES Tables/CountryCodesTable.cpp Tables/SMSTemplateTable.cpp Tables/NotificationsTable.cpp + Tables/MultimediaFilesTable.cpp Interface/AlarmEventRecord.cpp Interface/EventRecord.cpp diff --git a/module-db/Databases/MultimediaFilesDB.cpp b/module-db/Databases/MultimediaFilesDB.cpp new file mode 100644 index 000000000..0ce92047c --- /dev/null +++ b/module-db/Databases/MultimediaFilesDB.cpp @@ -0,0 +1,7 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include "MultimediaFilesDB.hpp" + +MultimediaFilesDB::MultimediaFilesDB(const char *name) : Database(name), files(this) +{} diff --git a/module-db/Databases/MultimediaFilesDB.hpp b/module-db/Databases/MultimediaFilesDB.hpp new file mode 100644 index 000000000..6d323a620 --- /dev/null +++ b/module-db/Databases/MultimediaFilesDB.hpp @@ -0,0 +1,15 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#pragma once + +#include +#include + +class MultimediaFilesDB : public Database +{ + public: + explicit MultimediaFilesDB(const char *name); + + MultimediaFilesTable files; +}; diff --git a/module-db/Tables/MultimediaFilesTable.cpp b/module-db/Tables/MultimediaFilesTable.cpp new file mode 100644 index 000000000..ee07ddc29 --- /dev/null +++ b/module-db/Tables/MultimediaFilesTable.cpp @@ -0,0 +1,188 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include "MultimediaFilesTable.hpp" + +#include +#include +#include + +MultimediaFilesTableRow CreateMultimediaFilesTableRow(const QueryResult &result) +{ + if (result.getFieldCount() != magic_enum::enum_count() + 1) { + return MultimediaFilesTableRow{}; + } + + return MultimediaFilesTableRow{ + result[0].getUInt32(), // ID + result[1].getString(), // path + result[2].getString(), // mediaType + result[3].getUInt32(), // size + {result[4].getString(), // title + result[5].getString(), // artist + result[6].getString(), // album + result[7].getString(), // comment + result[8].getString(), // genre + result[9].getUInt32(), // year + result[10].getUInt32()}, // track + {result[11].getUInt32(), // songLength + result[12].getUInt32(), // bitrate + result[13].getUInt32(), // sample rate + result[14].getUInt32()}, // channels + }; +} + +namespace +{ + std::vector retQueryUnpack(std::unique_ptr retQuery) + { + if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) { + return {}; + } + + std::vector outVector; + + do { + outVector.push_back(CreateMultimediaFilesTableRow(*retQuery)); + } while (retQuery->nextRow()); + return outVector; + } +} // namespace + +auto MultimediaFilesTableRow::isValid() const -> bool +{ + return (!path.empty() && Record::isValid()); +} + +MultimediaFilesTable::MultimediaFilesTable(Database *db) : Table(db) +{} + +bool MultimediaFilesTable::create() +{ + return true; +} + +bool MultimediaFilesTable::add(MultimediaFilesTableRow entry) +{ + return db->execute("INSERT or ignore INTO files (path, media_type, size, title, artist, album," + "comment, genre, year, track, song_length, bitrate, sample_rate, channels)" + "VALUES('%q', '%q', %lu, '%q', '%q', '%q', '%q', '%q', %lu, %lu, %lu, %lu, %lu, %lu);", + entry.path.c_str(), + entry.mediaType.c_str(), + entry.size, + entry.tags.title.c_str(), + entry.tags.artist.c_str(), + entry.tags.album.c_str(), + entry.tags.comment.c_str(), + entry.tags.genre.c_str(), + entry.tags.year, + entry.tags.track, + entry.audioProperties.songLength, + entry.audioProperties.bitrate, + entry.audioProperties.sampleRate, + entry.audioProperties.channels); +} + +bool MultimediaFilesTable::removeById(uint32_t id) +{ + return db->execute("DELETE FROM files WHERE _id = %lu;", id); +} + +bool MultimediaFilesTable::removeByField(MultimediaFilesTableFields field, const char *str) +{ + const auto &fieldName = getFieldName(field); + + if (fieldName.empty()) { + return false; + } + + return db->execute("DELETE FROM files WHERE %q = '%q';", fieldName.c_str(), str); +} + +bool MultimediaFilesTable::removeAll() +{ + return db->execute("DELETE FROM files;"); +} + +bool MultimediaFilesTable::update(MultimediaFilesTableRow entry) +{ + return db->execute("UPDATE files SET path = '%q', media_type = '%q', size = %lu, title = '%q', artist = '%q'," + "album = '%q', comment = '%q', genre = '%q', year = %lu, track = %lu, song_length = %lu," + "bitrate = %lu, sample_rate = %lu, channels = %lu WHERE _id = %lu;", + entry.path.c_str(), + entry.mediaType.c_str(), + entry.size, + entry.tags.title.c_str(), + entry.tags.artist.c_str(), + entry.tags.album.c_str(), + entry.tags.comment.c_str(), + entry.tags.genre.c_str(), + entry.tags.year, + entry.tags.track, + entry.audioProperties.songLength, + entry.audioProperties.bitrate, + entry.audioProperties.sampleRate, + entry.audioProperties.channels, + entry.ID); +} + +MultimediaFilesTableRow MultimediaFilesTable::getById(uint32_t id) +{ + auto retQuery = db->query("SELECT * FROM files WHERE _id = %lu;", id); + + if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) { + return MultimediaFilesTableRow(); + } + + return CreateMultimediaFilesTableRow(*retQuery); +} + +std::vector MultimediaFilesTable::getLimitOffset(uint32_t offset, uint32_t limit) +{ + auto retQuery = db->query("SELECT * from files LIMIT %lu OFFSET %lu;", limit, offset); + + return retQueryUnpack(std::move(retQuery)); +} + +std::vector MultimediaFilesTable::getLimitOffsetByField(uint32_t offset, + uint32_t limit, + MultimediaFilesTableFields field, + const char *str) +{ + std::unique_ptr retQuery = nullptr; + const auto &fieldName = getFieldName(field); + + if (fieldName.empty()) { + return {}; + } + + retQuery = + db->query("SELECT * FROM files WHERE %q = '%q' LIMIT %lu OFFSET %lu;", fieldName.c_str(), str, limit, offset); + + return retQueryUnpack(std::move(retQuery)); +} + +uint32_t MultimediaFilesTable::count() +{ + auto queryRet = db->query("SELECT COUNT(*) FROM files;"); + if (!queryRet || queryRet->getRowCount() == 0) { + return 0; + } + + return (*queryRet)[0].getUInt32(); +} + +uint32_t MultimediaFilesTable::countByFieldId(const char *field, uint32_t id) +{ + auto queryRet = db->query("SELECT COUNT(*) FROM files WHERE %q=%lu;", field, id); + if ((queryRet == nullptr) || (queryRet->getRowCount() == 0)) { + return 0; + } + + return (*queryRet)[0].getUInt32(); +} + +std::string MultimediaFilesTable::getFieldName(MultimediaFilesTableFields field) +{ + return utils::enumToString(field); +} diff --git a/module-db/Tables/MultimediaFilesTable.hpp b/module-db/Tables/MultimediaFilesTable.hpp new file mode 100644 index 000000000..a366188a2 --- /dev/null +++ b/module-db/Tables/MultimediaFilesTable.hpp @@ -0,0 +1,78 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#pragma once + +#include "EventsTable.hpp" + +#include +#include + +#include + +struct MultimediaFilesTableRow : public Record +{ + std::string path; + std::string mediaType; /// mime type e.g. "audio/mp3" + std::size_t size{}; + struct + { + std::string title; + std::string artist; + std::string album; + std::string comment; + std::string genre; + unsigned year{}; + unsigned track{}; + } tags; + struct + { + unsigned songLength{}; + unsigned bitrate{}; + unsigned sampleRate{}; + unsigned channels{}; /// 1 - mono, 2 - stereo + } audioProperties; + + auto isValid() const -> bool; +}; + +enum class MultimediaFilesTableFields +{ + path, + media_type, + size, + title, + artist, + album, + comment, + genre, + year, + track, + song_length, + bitrate, + sample_rate, + channels +}; + +class MultimediaFilesTable : public Table +{ + public: + explicit MultimediaFilesTable(Database *db); + virtual ~MultimediaFilesTable() = default; + + auto create() -> bool override; + auto add(MultimediaFilesTableRow entry) -> bool override; + auto removeById(uint32_t id) -> bool override; + auto removeByField(MultimediaFilesTableFields field, const char *str) -> bool override; + bool removeAll() override final; + auto update(MultimediaFilesTableRow entry) -> bool override; + auto getById(uint32_t id) -> MultimediaFilesTableRow override; + auto getLimitOffset(uint32_t offset, uint32_t limit) -> std::vector override; + auto getLimitOffsetByField(uint32_t offset, uint32_t limit, MultimediaFilesTableFields field, const char *str) + -> std::vector override; + auto count() -> uint32_t override; + auto countByFieldId(const char *field, uint32_t id) -> uint32_t override; + + private: + auto getFieldName(MultimediaFilesTableFields field) -> std::string; +}; diff --git a/module-db/tests/CMakeLists.txt b/module-db/tests/CMakeLists.txt index f89867cab..37fe8489f 100644 --- a/module-db/tests/CMakeLists.txt +++ b/module-db/tests/CMakeLists.txt @@ -16,6 +16,7 @@ add_catch2_executable( ContactsTable_tests.cpp DbInitializer.cpp EventRecord_tests.cpp + MultimediaFilesTable_tests.cpp NotesRecord_tests.cpp NotesTable_tests.cpp NotificationsRecord_tests.cpp diff --git a/module-db/tests/MultimediaFilesTable_tests.cpp b/module-db/tests/MultimediaFilesTable_tests.cpp new file mode 100644 index 000000000..079fd9359 --- /dev/null +++ b/module-db/tests/MultimediaFilesTable_tests.cpp @@ -0,0 +1,149 @@ +// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved. +// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md + +#include + +#include + +constexpr auto artist1 = "Super artist"; +constexpr auto artist2 = "Mega artist"; +constexpr auto artist3 = "Just an artist"; + +constexpr auto song1 = "Super song"; +constexpr auto song2 = "Mega song"; +constexpr auto song3 = "Just a song"; + +constexpr auto album1 = "Super album"; +constexpr auto album2 = "Mega album"; +constexpr auto album3 = "Just an album"; + +const MultimediaFilesTableRow records[] = { + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song1, .artist = artist1, .album = album1, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}, + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song2, .artist = artist1, .album = album1, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}, + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song2, .artist = artist1, .album = album2, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}, + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song2, .artist = artist2, .album = album1, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}, + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song3, .artist = artist3, .album = album2, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}, + {Record{DB_ID_NONE}, + .path = "user/music", + .mediaType = "audio/mp3", + .size = 100, + .tags = {.title = song3, .artist = artist2, .album = album3, .comment = "", .genre = "", .year = 2011, .track = 1}, + .audioProperties = {.songLength = 300, .bitrate = 320, .sampleRate = 44100, .channels = 1}}}; + +TEST_CASE("Multimedia DB tests") +{ + REQUIRE(Database::initialize()); + + const auto path = (std::filesystem::path{"sys/user"} / "multimedia.db"); + if (std::filesystem::exists(path)) { + REQUIRE(std::filesystem::remove(path)); + } + + MultimediaFilesDB db(path.c_str()); + REQUIRE(db.isInitialized()); + + constexpr auto PageSize = 8; + + SECTION("MultimediaFilesTableRow") + { + auto record = MultimediaFilesTableRow{}; + REQUIRE(!record.isValid()); + + record.ID = 1; + record.path = "music"; + + REQUIRE(record.isValid()); + } + + SECTION("Empty") + { + REQUIRE(db.files.count() == 0); + REQUIRE(db.files.getLimitOffset(0, PageSize).empty()); + } + + SECTION("Add, get and remove") + { + const auto path = "music/user"; + MultimediaFilesTableRow record; + record.path = path; + REQUIRE(!record.isValid()); + REQUIRE(db.files.add(record)); + + REQUIRE(db.files.count() == 1); + auto result = db.files.getById(1); + REQUIRE(result.isValid()); + + SECTION("Remove by ID") + { + REQUIRE(db.files.removeById(1)); + REQUIRE(db.files.count() == 0); + auto result = db.files.getById(1); + REQUIRE(!result.isValid()); + } + SECTION("Remove by field") + { + REQUIRE(db.files.removeByField(MultimediaFilesTableFields::path, path)); + REQUIRE(db.files.count() == 0); + auto result = db.files.getById(1); + REQUIRE(!result.isValid()); + } + } + + for (const auto &record : records) { + REQUIRE(db.files.add(record)); + } + + SECTION("Remove all") + { + REQUIRE(db.files.count() == 6); + REQUIRE(db.files.removeAll()); + REQUIRE(db.files.count() == 0); + } + + SECTION("Update") + { + auto resultPre = db.files.getById(2); + resultPre.mediaType = "bla bla"; + REQUIRE(db.files.update(resultPre)); + auto resultPost = db.files.getById(2); + REQUIRE((resultPre.ID == resultPost.ID && resultPre.mediaType == resultPost.mediaType)); + } + + SECTION("getLimitOffset") + { + REQUIRE(db.files.getLimitOffset(0, 4).size() == 4); + REQUIRE(db.files.getLimitOffset(4, 4).size() == 2); + } + + SECTION("getLimitOffsetByField") + { + REQUIRE(db.files.getLimitOffsetByField(0, 4, MultimediaFilesTableFields::artist, artist1).size() == 3); + } + + REQUIRE(Database::deinitialize()); +}