~aleteoryx/muditaos

b4828512694ffccdd8d53ccff6c8c5a4486090cf — Alek Rudnik 4 years ago 8f10062
[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.
A image/user/db/multimedia_001.sql => image/user/db/multimedia_001.sql +21 -0
@@ 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
);

M module-db/CMakeLists.txt => module-db/CMakeLists.txt +6 -4
@@ 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

A module-db/Databases/MultimediaFilesDB.cpp => module-db/Databases/MultimediaFilesDB.cpp +7 -0
@@ 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)
{}

A module-db/Databases/MultimediaFilesDB.hpp => module-db/Databases/MultimediaFilesDB.hpp +15 -0
@@ 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 <Database/Database.hpp>
#include <Tables/MultimediaFilesTable.hpp>

class MultimediaFilesDB : public Database
{
  public:
    explicit MultimediaFilesDB(const char *name);

    MultimediaFilesTable files;
};

A module-db/Tables/MultimediaFilesTable.cpp => module-db/Tables/MultimediaFilesTable.cpp +188 -0
@@ 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 <Interface/AlarmEventRecord.hpp>
#include <Utils.hpp>
#include <magic_enum.hpp>

MultimediaFilesTableRow CreateMultimediaFilesTableRow(const QueryResult &result)
{
    if (result.getFieldCount() != magic_enum::enum_count<MultimediaFilesTableFields>() + 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<MultimediaFilesTableRow> retQueryUnpack(std::unique_ptr<QueryResult> retQuery)
    {
        if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
            return {};
        }

        std::vector<MultimediaFilesTableRow> 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<MultimediaFilesTableRow> 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<MultimediaFilesTableRow> MultimediaFilesTable::getLimitOffsetByField(uint32_t offset,
                                                                                 uint32_t limit,
                                                                                 MultimediaFilesTableFields field,
                                                                                 const char *str)
{
    std::unique_ptr<QueryResult> 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);
}

A module-db/Tables/MultimediaFilesTable.hpp => module-db/Tables/MultimediaFilesTable.hpp +78 -0
@@ 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 <Common/Common.hpp>
#include <Database/Database.hpp>

#include <string>

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<MultimediaFilesTableRow, MultimediaFilesTableFields>
{
  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<MultimediaFilesTableRow> override;
    auto getLimitOffsetByField(uint32_t offset, uint32_t limit, MultimediaFilesTableFields field, const char *str)
        -> std::vector<MultimediaFilesTableRow> override;
    auto count() -> uint32_t override;
    auto countByFieldId(const char *field, uint32_t id) -> uint32_t override;

  private:
    auto getFieldName(MultimediaFilesTableFields field) -> std::string;
};

M module-db/tests/CMakeLists.txt => module-db/tests/CMakeLists.txt +1 -0
@@ 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

A module-db/tests/MultimediaFilesTable_tests.cpp => module-db/tests/MultimediaFilesTable_tests.cpp +149 -0
@@ 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 <catch2/catch.hpp>

#include <Databases/MultimediaFilesDB.hpp>

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());
}