~aleteoryx/muditaos

c69fde01d4d07cadb195c7febc32be8a875bc4de — Maciej Gibowicz 4 years ago b61ac5c
[EGD-6630] Integration file indexer with music player

The list of music tracks is retrieved from the database,
where they are indexed beforehand,
instead of being searched in real time in the file system.
M module-apps/application-music-player/ApplicationMusicPlayer.cpp => module-apps/application-music-player/ApplicationMusicPlayer.cpp +2 -2
@@ 46,8 46,8 @@ namespace app
        bus.channels.push_back(sys::BusChannel::ServiceAudioNotifications);

        auto tagsFetcher     = std::make_unique<app::music_player::ServiceAudioTagsFetcher>(this);
        auto songsRepository = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcher));
        priv->songsModel     = std::make_unique<app::music_player::SongsModel>(std::move(songsRepository));
        auto songsRepository = std::make_unique<app::music_player::SongsRepository>(this, std::move(tagsFetcher));
        priv->songsModel     = std::make_unique<app::music_player::SongsModel>(this, std::move(songsRepository));
        auto audioOperations = std::make_unique<app::AsyncAudioOperations>(this);
        priv->songsPresenter =
            std::make_unique<app::music_player::SongsPresenter>(priv->songsModel, std::move(audioOperations));

M module-apps/application-music-player/models/SongsModel.cpp => module-apps/application-music-player/models/SongsModel.cpp +71 -64
@@ 12,13 12,19 @@
namespace app::music_player
{

    SongsModel::SongsModel(std::shared_ptr<AbstractSongsRepository> songsRepository)
        : songsRepository{std::move(songsRepository)}
    SongsListItemProvider::SongsListItemProvider(ApplicationCommon *app) : DatabaseModel(app)
    {}

    SongsModelInterface::SongsModelInterface(ApplicationCommon *app) : SongsListItemProvider(app)
    {}

    SongsModel::SongsModel(app::ApplicationCommon *app, std::shared_ptr<AbstractSongsRepository> songsRepository)
        : SongsModelInterface(app), songsRepository{std::move(songsRepository)}
    {}

    auto SongsModel::requestRecordsCount() -> unsigned int
    {
        return internalData.size();
        return recordsCount;
    }

    auto SongsModel::getMinimalItemSpaceRequired() const -> unsigned int


@@ 28,48 34,65 @@ namespace app::music_player

    void SongsModel::requestRecords(const uint32_t offset, const uint32_t limit)
    {
        setupModel(offset, limit);
        list->onProviderDataUpdate();
        songsRepository->getMusicFilesList(
            offset,
            limit,
            [this](const std::vector<db::multimedia_files::MultimediaFilesRecord> &records,
                   unsigned int repoRecordsCount) { return onMusicListRetrieved(records, repoRecordsCount); });
    }

    auto SongsModel::getItem(gui::Order order) -> gui::ListItem *
    {
        return getRecord(order);
    }
        std::shared_ptr<db::multimedia_files::MultimediaFilesRecord> song = getRecord(order);
        if (!song) {
            return nullptr;
        }

    void SongsModel::createData(OnShortReleaseCallback shortReleaseCallback,
                                OnLongPressCallback longPressCallback,
                                OnSetBottomBarTemporaryCallback bottomBarTemporaryMode,
                                OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode)
    {
        songsRepository->scanMusicFilesList();
        auto songsList = songsRepository->getMusicFilesList();
        for (const auto &song : songsList) {
            auto item = new gui::SongItem(song.artist,
                                          song.title,
                                          utils::time::Duration(song.total_duration_s).str(),
                                          bottomBarTemporaryMode,
                                          bottomBarRestoreFromTemporaryMode);

            item->activatedCallback = [=](gui::Item &) {
                shortReleaseCallback(song.filePath);
        auto item = new gui::SongItem(song->tags.album.artist,
                                      song->tags.title,
                                      utils::time::Duration(song->audioProperties.songLength).str(),
                                      bottomBarTemporaryMode,
                                      bottomBarRestoreFromTemporaryMode);

        if (songContext.filePath == song->fileInfo.path) {
            item->setState(songContext.isPlaying() ? gui::SongItem::ItemState::Playing
                                                   : (songContext.isPaused() ? gui::SongItem::ItemState::Paused
                                                                             : gui::SongItem::ItemState::None));
        }
        else {
            item->setState(gui::SongItem::ItemState::None);
        }

        item->activatedCallback = [this, song](gui::Item &) {
            if (shortReleaseCallback != nullptr) {
                shortReleaseCallback(song->fileInfo.path);
                return true;
            };
            }
            return false;
        };

            item->inputCallback = [longPressCallback](gui::Item &, const gui::InputEvent &event) {
                if (event.isLongRelease(gui::KeyCode::KEY_ENTER)) {
        item->inputCallback = [=](gui::Item &, const gui::InputEvent &event) {
            if (event.isLongRelease(gui::KeyCode::KEY_ENTER)) {
                if (longPressCallback != nullptr) {
                    longPressCallback();
                    return true;
                }
                return false;
            };
            }
            return false;
        };

            internalData.push_back(item);
        }
        return item;
    }

        for (auto &item : internalData) {
            item->deleteByList = false;
        }
    void SongsModel::createData(OnShortReleaseCallback shortReleaseCallback,
                                OnLongPressCallback longPressCallback,
                                OnSetBottomBarTemporaryCallback bottomBarTemporaryMode,
                                OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode)
    {
        this->shortReleaseCallback              = shortReleaseCallback;
        this->longPressCallback                 = longPressCallback;
        this->bottomBarTemporaryMode            = bottomBarTemporaryMode;
        this->bottomBarRestoreFromTemporaryMode = bottomBarRestoreFromTemporaryMode;
    }

    bool SongsModel::isSongPlaying() const noexcept


@@ 80,7 103,6 @@ namespace app::music_player
    void SongsModel::setCurrentSongState(SongState songState) noexcept
    {
        songContext.currentSongState = songState;
        updateCurrentItemState();
    }

    std::optional<audio::Token> SongsModel::getCurrentFileToken() const noexcept


@@ 112,50 134,35 @@ namespace app::music_player
    void SongsModel::setCurrentSongContext(SongContext context)
    {
        using namespace gui;
        clearCurrentItemState();

        songContext = context;

        updateCurrentItemState();
    }

    void SongsModel::clearCurrentSongContext()
    {
        clearCurrentItemState();
        songContext.clear();
    }

    void SongsModel::clearCurrentItemState()
    void SongsModel::clearData()
    {
        using namespace gui;
        const auto songIndex = getCurrentIndex();
        if (songIndex < internalData.size()) {
            internalData[songIndex]->setState(SongItem::ItemState::None);
        }
        list->reset();
    }

    void SongsModel::updateCurrentItemState()
    [[nodiscard]] bool SongsModel::updateRecords(std::vector<db::multimedia_files::MultimediaFilesRecord> records)
    {
        using namespace gui;
        const auto songIndex = getCurrentIndex();
        if (songIndex >= internalData.size()) {
            return;
        }

        if (songContext.isPlaying()) {
            internalData[songIndex]->setState(SongItem::ItemState::Playing);
        }
        else if (songContext.isPaused()) {
            internalData[songIndex]->setState(SongItem::ItemState::Paused);
        }
        else {
            internalData[songIndex]->setState(SongItem::ItemState::None);
        }
        DatabaseModel::updateRecords(std::move(records));
        list->onProviderDataUpdate();
        return true;
    }

    void SongsModel::clearData()
    bool SongsModel::onMusicListRetrieved(const std::vector<db::multimedia_files::MultimediaFilesRecord> &records,
                                          unsigned int repoRecordsCount)
    {
        list->reset();
        eraseInternalData();
        if (recordsCount != repoRecordsCount) {
            recordsCount = repoRecordsCount;
            list->reSendLastRebuildRequest();
            return false;
        }
        return updateRecords(records);
    }

} // namespace app::music_player

M module-apps/application-music-player/models/SongsModel.hpp => module-apps/application-music-player/models/SongsModel.hpp +10 -3
@@ 16,7 16,7 @@ namespace app::music_player
    class SongsModel : public SongsModelInterface
    {
      public:
        explicit SongsModel(std::shared_ptr<AbstractSongsRepository> songsRepository);
        SongsModel(app::ApplicationCommon *app, std::shared_ptr<AbstractSongsRepository> songsRepository);

        void createData(OnShortReleaseCallback shortReleaseCallback,
                        OnLongPressCallback longPressCallback,


@@ 46,10 46,17 @@ namespace app::music_player
        void clearData() override;

      private:
        void clearCurrentItemState();
        void updateCurrentItemState();
        bool onMusicListRetrieved(const std::vector<db::multimedia_files::MultimediaFilesRecord> &records,
                                  unsigned int repoRecordsCount);
        [[nodiscard]] bool updateRecords(std::vector<db::multimedia_files::MultimediaFilesRecord> records) override;

        SongContext songContext;

        OnShortReleaseCallback shortReleaseCallback{nullptr};
        OnLongPressCallback longPressCallback{nullptr};
        OnSetBottomBarTemporaryCallback bottomBarTemporaryMode{nullptr};
        OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode{nullptr};

        std::shared_ptr<AbstractSongsRepository> songsRepository;
    };
} // namespace app::music_player

M module-apps/application-music-player/models/SongsModelInterface.hpp => module-apps/application-music-player/models/SongsModelInterface.hpp +6 -3
@@ 6,20 6,22 @@
#include "SongContext.hpp"
#include <string>
#include <widgets/SongItem.hpp>
#include <InternalModel.hpp>
#include <ListItemProvider.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <apps-common/DatabaseModel.hpp>
#include <module-db/Interface/MultimediaFilesRecord.hpp>

namespace app::music_player
{
    class SongsListItemProvider : public app::InternalModel<gui::SongItem *>, public gui::ListItemProvider
    class SongsListItemProvider : public app::DatabaseModel<db::multimedia_files::MultimediaFilesRecord>,
                                  public gui::ListItemProvider
    {
      public:
        using OnShortReleaseCallback              = std::function<bool(const std::string &fileName)>;
        using OnLongPressCallback                 = std::function<void()>;
        using OnSetBottomBarTemporaryCallback     = std::function<void(const UTF8 &)>;
        using OnRestoreBottomBarTemporaryCallback = std::function<void()>;

        explicit SongsListItemProvider(app::ApplicationCommon *app);
        virtual ~SongsListItemProvider() noexcept = default;

        virtual void createData(OnShortReleaseCallback shortReleaseCallback,


@@ 33,6 35,7 @@ namespace app::music_player
    class SongsModelInterface : public SongsListItemProvider
    {
      public:
        explicit SongsModelInterface(app::ApplicationCommon *app);
        virtual ~SongsModelInterface() noexcept = default;

        virtual bool isSongPlaying() const noexcept                              = 0;

M module-apps/application-music-player/models/SongsRepository.cpp => module-apps/application-music-player/models/SongsRepository.cpp +28 -33
@@ 9,7 9,7 @@
#include <service-audio/AudioServiceName.hpp>
#include <time/ScopedTime.hpp>
#include <service-audio/AudioMessage.hpp>
#include <tags_fetcher/TagsFetcher.hpp>
#include <module-db/queries/multimedia_files/QueryMultimediaFilesGetLimited.hpp>

#include <filesystem>



@@ 23,46 23,40 @@ namespace app::music_player
        return tags::fetcher::fetchTags(filePath);
    }

    SongsRepository::SongsRepository(std::unique_ptr<AbstractTagsFetcher> tagsFetcher, std::string musicFolderName)
        : tagsFetcher(std::move(tagsFetcher)), musicFolderName(std::move(musicFolderName))
    SongsRepository::SongsRepository(ApplicationCommon *application, std::unique_ptr<AbstractTagsFetcher> tagsFetcher)
        : app::AsyncCallbackReceiver{application}, application{application}, tagsFetcher(std::move(tagsFetcher))
    {}

    void SongsRepository::scanMusicFilesList()
    void SongsRepository::getMusicFilesList(std::uint32_t offset,
                                            std::uint32_t limit,
                                            const OnGetMusicFilesListCallback &callback)
    {
        musicFiles.clear();

        LOG_INFO("Scanning music folder: %s", musicFolderName.c_str());
        {
            auto time = utils::time::Scoped("fetch tags time");
            for (const auto &entry : std::filesystem::directory_iterator(musicFolderName)) {
                if (!std::filesystem::is_directory(entry)) {
                    const auto &filePath = entry.path();
                    const auto fileTags  = tagsFetcher->getFileTags(filePath);
                    if (fileTags) {
                        musicFiles.push_back(*fileTags);
                    }
                    else {
                        LOG_ERROR("Scanned not an audio file, skipped");
                    }
                }
        auto query = std::make_unique<db::multimedia_files::query::GetLimited>(offset, limit);
        auto task  = app::AsyncQuery::createFromQuery(std::move(query), db::Interface::Name::MultimediaFiles);

        task->setCallback([this, callback](auto response) {
            auto result = dynamic_cast<db::multimedia_files::query::GetLimitedResult *>(response);
            musicFiles.clear();

            if (result == nullptr) {
                return false;
            }
            std::sort(
                musicFiles.begin(), musicFiles.end(), [](const tags::fetcher::Tags &t1, const tags::fetcher::Tags &t2) {
                    return t1.filePath < t2.filePath;
                });
        }
        LOG_INFO("Total number of music files found: %u", static_cast<unsigned int>(musicFiles.size()));
    }

    std::vector<tags::fetcher::Tags> SongsRepository::getMusicFilesList() const
    {
        return musicFiles;
            if (callback) {
                for (auto &record : result->getResult()) {
                    musicFiles.push_back(record);
                }
                callback(musicFiles, result->getCount());
            }
            return true;
        });
        task->execute(application, this);
    }

    std::size_t SongsRepository::getFileIndex(const std::string &filePath) const
    {
        auto it = std::find_if(musicFiles.begin(), musicFiles.end(), [filePath](const auto &musicFile) {
            return musicFile.filePath == filePath;
            return musicFile.fileInfo.path == filePath;
        });

        if (it != musicFiles.end()) {


@@ 79,7 73,7 @@ namespace app::music_player
        if (currentIndex == std::numeric_limits<size_t>::max() || currentIndex == musicFiles.size() - 1) {
            return "";
        }
        return musicFiles[currentIndex + 1].filePath;
        return musicFiles[currentIndex + 1].fileInfo.path;
    }

    std::string SongsRepository::getPreviousFilePath(const std::string &filePath) const


@@ 89,6 83,7 @@ namespace app::music_player
        if (currentIndex == std::numeric_limits<size_t>::max() || currentIndex == 0) {
            return "";
        }
        return musicFiles[currentIndex - 1].filePath;
        return musicFiles[currentIndex - 1].fileInfo.path;
    }

} // namespace app::music_player

M module-apps/application-music-player/models/SongsRepository.hpp => module-apps/application-music-player/models/SongsRepository.hpp +15 -12
@@ 5,7 5,7 @@

#include <apps-common/ApplicationCommon.hpp>
#include <tags_fetcher/TagsFetcher.hpp>
#include <purefs/filesystem_paths.hpp>
#include <module-db/Interface/MultimediaFilesRecord.hpp>

#include <memory>
#include <optional>


@@ 38,32 38,35 @@ namespace app::music_player
    class AbstractSongsRepository
    {
      public:
        using OnGetMusicFilesListCallback =
            std::function<bool(const std::vector<db::multimedia_files::MultimediaFilesRecord> &, unsigned int)>;

        virtual ~AbstractSongsRepository() noexcept = default;

        virtual void scanMusicFilesList()                                          = 0;
        virtual std::vector<tags::fetcher::Tags> getMusicFilesList() const         = 0;
        virtual void getMusicFilesList(std::uint32_t offset,
                                       std::uint32_t limit,
                                       const OnGetMusicFilesListCallback &callback) = 0;
        virtual std::size_t getFileIndex(const std::string &filePath) const        = 0;
        virtual std::string getNextFilePath(const std::string &filePath) const     = 0;
        virtual std::string getPreviousFilePath(const std::string &filePath) const = 0;
    };

    class SongsRepository : public AbstractSongsRepository
    class SongsRepository : public AbstractSongsRepository, public app::AsyncCallbackReceiver
    {
        static constexpr auto musicSubfolderName = "music";

      public:
        explicit SongsRepository(std::unique_ptr<AbstractTagsFetcher> tagsFetcher,
                                 std::string musicFolderName = purefs::dir::getUserDiskPath() / musicSubfolderName);
        explicit SongsRepository(ApplicationCommon *application, std::unique_ptr<AbstractTagsFetcher> tagsFetcher);

        void scanMusicFilesList() override;
        std::vector<tags::fetcher::Tags> getMusicFilesList() const override;
        void getMusicFilesList(std::uint32_t offset,
                               std::uint32_t limit,
                               const OnGetMusicFilesListCallback &callback) override;
        std::size_t getFileIndex(const std::string &filePath) const override;
        std::string getNextFilePath(const std::string &filePath) const override;
        std::string getPreviousFilePath(const std::string &filePath) const override;

      private:
        ApplicationCommon *application;

        std::unique_ptr<AbstractTagsFetcher> tagsFetcher;
        std::string musicFolderName;
        std::vector<tags::fetcher::Tags> musicFiles;
        std::vector<db::multimedia_files::MultimediaFilesRecord> musicFiles;
    };
} // namespace app::music_player

M module-apps/application-music-player/tests/CMakeLists.txt => module-apps/application-music-player/tests/CMakeLists.txt +0 -1
@@ 3,7 3,6 @@ add_gtest_executable(
        app-music-player
    SRCS
        unittest.cpp
        unittest_songrepository.cpp
        unittest_songsmodel.cpp
    LIBS
        application-music-player

M module-apps/application-music-player/tests/MockSongsRepository.hpp => module-apps/application-music-player/tests/MockSongsRepository.hpp +4 -2
@@ 17,8 17,10 @@ namespace testing::app::music_player
    class MockSongsRepository : public ::app::music_player::AbstractSongsRepository
    {
      public:
        MOCK_METHOD(void, scanMusicFilesList, (), (override));
        MOCK_METHOD(std::vector<tags::fetcher::Tags>, getMusicFilesList, (), (const override));
        MOCK_METHOD(void,
                    getMusicFilesList,
                    (std::uint32_t offset, std::uint32_t limit, const OnGetMusicFilesListCallback &callback),
                    (override));
        MOCK_METHOD(std::size_t, getFileIndex, (const std::string &filePath), (const override));
        MOCK_METHOD(std::string, getNextFilePath, (const std::string &filePath), (const override));
        MOCK_METHOD(std::string, getPreviousFilePath, (const std::string &filePath), (const override));

D module-apps/application-music-player/tests/unittest_songrepository.cpp => module-apps/application-music-player/tests/unittest_songrepository.cpp +0 -154
@@ 1,154 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include "MockTagsFetcher.hpp"

#include <models/SongsRepository.hpp>
#include <tags_fetcher/TagsFetcher.hpp>
#include <filesystem>
#include <fstream>
#include <stdexcept>

using ::testing::Return;
using ::testing::app::music_player::MockTagsFetcher;
namespace fs = std::filesystem;

constexpr auto testDir  = "appmusic-test";
constexpr auto emptyDir = "empty";
constexpr auto musicDir = "music";
constexpr auto bazDir   = "bazdir";
auto testDirPath        = fs::path(testDir);
auto emptyDirPath       = testDirPath / emptyDir;
auto musicDirPath       = testDirPath / musicDir;
auto bazDirPath         = musicDirPath / bazDir;

class SongsRepositoryFixture : public ::testing::Test
{
  protected:
    static void SetUpTestSuite()
    {
        if (fs::exists(testDirPath)) {
            TearDownTestSuite();
        }

        fs::create_directory(testDirPath);
        fs::create_directory(emptyDirPath);
        fs::create_directory(musicDirPath);

        createFile(musicDirPath / "foo");
        createFile(musicDirPath / "bar");

        fs::create_directory(bazDirPath);

        createFile(bazDirPath / "baz");

        fs::create_directory(musicDirPath / "bazzinga");
    }

    static void createFile(const std::string &path)
    {
        std::ofstream file(path);
        file << "app music test file";
    }

    static void TearDownTestSuite()
    {
        fs::remove_all(testDir);
    }

    auto getMockedRepository(const std::string &directoryToScan)
    {
        return std::make_unique<app::music_player::SongsRepository>(std::make_unique<MockTagsFetcher>(),
                                                                    directoryToScan);
    }
};

TEST_F(SongsRepositoryFixture, LazyInit)
{
    auto repo       = getMockedRepository(testDir);
    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, Empty)
{
    auto repo = getMockedRepository(testDirPath / emptyDir);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, ScanEmptyFiles)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, ScanWithTagsReturn)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    auto fooTags = tags::fetcher::Tags{"foo"};
    auto barTags = tags::fetcher::Tags{"foo"};

    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "foo").c_str())).WillByDefault(Return(fooTags));
    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "bar").c_str())).WillByDefault(Return(barTags));
    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 2);
}

TEST_F(SongsRepositoryFixture, FileIndex)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    auto fooPath = musicDirPath / "foo";
    auto barPath = musicDirPath / "bar";

    auto fooTags = tags::fetcher::Tags(fooPath);
    auto barTags = tags::fetcher::Tags(barPath);

    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "foo").c_str())).WillByDefault(Return(fooTags));
    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "bar").c_str())).WillByDefault(Return(barTags));
    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto fooIndex = repo->getFileIndex(fooPath);
    auto barIndex = repo->getFileIndex(barPath);

    EXPECT_NE(fooIndex, static_cast<std::size_t>(-1));
    EXPECT_EQ(fooIndex, 1);

    EXPECT_NE(barIndex, static_cast<std::size_t>(-1));
    EXPECT_EQ(barIndex, 0);

    auto bazIndex = repo->getFileIndex("baz");
    EXPECT_EQ(bazIndex, static_cast<std::size_t>(-1));

    EXPECT_EQ(repo->getNextFilePath(barTags.filePath), fooTags.filePath);
    EXPECT_EQ(repo->getNextFilePath(fooTags.filePath), "");
    EXPECT_EQ(repo->getNextFilePath("rand"), "");
    EXPECT_EQ(repo->getNextFilePath(""), "");

    EXPECT_EQ(repo->getPreviousFilePath(barTags.filePath), "");
    EXPECT_EQ(repo->getPreviousFilePath(fooTags.filePath), barTags.filePath);
    EXPECT_EQ(repo->getPreviousFilePath("rand"), "");
    EXPECT_EQ(repo->getPreviousFilePath(""), "");
}

M module-apps/application-music-player/tests/unittest_songsmodel.cpp => module-apps/application-music-player/tests/unittest_songsmodel.cpp +2 -13
@@ 17,16 17,15 @@ using ::testing::app::music_player::MockSongsRepository;
TEST(SongsModel, Init)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);
    auto model    = SongsModel(nullptr, mockRepo);

    EXPECT_EQ(model.requestRecordsCount(), 0);
    EXPECT_FALSE(model.isSongPlaying());
}

TEST(SongsModel, EmptyContext)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);
    auto model    = SongsModel(nullptr, mockRepo);

    auto ctx = model.getCurrentSongContext();



@@ 34,13 33,3 @@ TEST(SongsModel, EmptyContext)
    EXPECT_TRUE(ctx.filePath.empty());
    EXPECT_EQ(ctx.currentSongState, app::music_player::SongState::NotPlaying);
}

TEST(SongsModel, createDataNoSongs)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);

    EXPECT_CALL(*mockRepo, scanMusicFilesList);
    EXPECT_CALL(*mockRepo, getMusicFilesList).WillRepeatedly(Return(std::vector<tags::fetcher::Tags>()));
    model.createData([](const std::string &) { return true; }, []() {}, [](const UTF8 &) {}, []() {});
}

M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp => module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp +0 -3
@@ 69,9 69,6 @@ namespace gui
    void MusicPlayerAllSongsWindow::onBeforeShow([[maybe_unused]] ShowMode mode, [[maybe_unused]] SwitchData *data)
    {
        presenter->attach(this);
        auto index = presenter->getMusicPlayerItemProvider()->getCurrentIndex();

        songsList->rebuildList(listview::RebuildType::OnPageElement, index);
    }

    void MusicPlayerAllSongsWindow::updateSongsState()

M module-db/Interface/MultimediaFilesRecord.cpp => module-db/Interface/MultimediaFilesRecord.cpp +5 -5
@@ 137,7 137,7 @@ namespace db::multimedia_files
    {
        const auto records = database->files.getLimitOffset(query->offset, query->limit);

        auto response = std::make_unique<query::GetLimitedResult>(records);
        auto response = std::make_unique<query::GetLimitedResult>(records, database->files.count());
        response->setRequestQuery(query);

        return response;


@@ 192,7 192,7 @@ namespace db::multimedia_files
    {
        const auto records = database->files.getArtistsLimitOffset(query->offset, query->limit);

        auto response = std::make_unique<query::GetArtistsLimitedResult>(records);
        auto response = std::make_unique<query::GetArtistsLimitedResult>(records, database->files.countArtists());
        response->setRequestQuery(query);

        return response;


@@ 212,7 212,7 @@ namespace db::multimedia_files
    {
        const auto records = database->files.getAlbumsLimitOffset(query->offset, query->limit);

        auto response = std::make_unique<query::GetAlbumsLimitedResult>(records);
        auto response = std::make_unique<query::GetAlbumsLimitedResult>(records, database->files.countAlbums());
        response->setRequestQuery(query);

        return response;


@@ 223,7 223,7 @@ namespace db::multimedia_files
    {
        const auto records = database->files.getLimitOffset(query->artist, query->offset, query->limit);

        auto response = std::make_unique<query::GetLimitedResult>(records);
        auto response = std::make_unique<query::GetLimitedResult>(records, database->files.count());
        response->setRequestQuery(query);

        return response;


@@ 243,7 243,7 @@ namespace db::multimedia_files
    {
        const auto records = database->files.getLimitOffset(query->album, query->offset, query->limit);

        auto response = std::make_unique<query::GetLimitedResult>(records);
        auto response = std::make_unique<query::GetLimitedResult>(records, database->files.count());
        response->setRequestQuery(query);

        return response;

M module-db/queries/multimedia_files/QueryMultimediaFilesGetLimited.cpp => module-db/queries/multimedia_files/QueryMultimediaFilesGetLimited.cpp +21 -3
@@ 31,7 31,8 @@ namespace db::multimedia_files::query
        return std::string{"GetLimitedForAlbum"};
    }

    GetLimitedResult::GetLimitedResult(std::vector<MultimediaFilesRecord> records) : records(std::move(records))
    GetLimitedResult::GetLimitedResult(std::vector<MultimediaFilesRecord> records, unsigned int dbRecordsCount)
        : records(std::move(records)), dbRecordsCount{dbRecordsCount}
    {}

    auto GetLimitedResult::getResult() const -> std::vector<MultimediaFilesRecord>


@@ 39,6 40,11 @@ namespace db::multimedia_files::query
        return records;
    }

    auto GetLimitedResult::getCount() const noexcept -> unsigned int
    {
        return dbRecordsCount;
    }

    auto GetLimitedResult::debugInfo() const -> std::string
    {
        return std::string{"GetLimitedResult"};


@@ 53,7 59,8 @@ namespace db::multimedia_files::query
        return std::string{"GetArtistsLimited"};
    }

    GetArtistsLimitedResult::GetArtistsLimitedResult(std::vector<Artist> records) : records(std::move(records))
    GetArtistsLimitedResult::GetArtistsLimitedResult(std::vector<Artist> records, unsigned int dbRecordsCount)
        : records(std::move(records)), dbRecordsCount{dbRecordsCount}
    {}

    auto GetArtistsLimitedResult::getResult() const -> std::vector<Artist>


@@ 61,6 68,11 @@ namespace db::multimedia_files::query
        return records;
    }

    auto GetArtistsLimitedResult::getCount() const noexcept -> unsigned int
    {
        return dbRecordsCount;
    }

    auto GetArtistsLimitedResult::debugInfo() const -> std::string
    {
        return std::string{"GetArtistsLimitedResult"};


@@ 75,7 87,8 @@ namespace db::multimedia_files::query
        return std::string{"GetAlbumsLimited"};
    }

    GetAlbumsLimitedResult::GetAlbumsLimitedResult(std::vector<Album> records) : records(std::move(records))
    GetAlbumsLimitedResult::GetAlbumsLimitedResult(std::vector<Album> records, unsigned int dbRecordsCount)
        : records(std::move(records)), dbRecordsCount{dbRecordsCount}
    {}

    auto GetAlbumsLimitedResult::getResult() const -> std::vector<Album>


@@ 83,6 96,11 @@ namespace db::multimedia_files::query
        return records;
    }

    auto GetAlbumsLimitedResult::getCount() const noexcept -> unsigned int
    {
        return dbRecordsCount;
    }

    auto GetAlbumsLimitedResult::debugInfo() const -> std::string
    {
        return std::string{"GetAlbumsLimitedResult"};

M module-db/queries/multimedia_files/QueryMultimediaFilesGetLimited.hpp => module-db/queries/multimedia_files/QueryMultimediaFilesGetLimited.hpp +9 -6
@@ 48,11 48,12 @@ namespace db::multimedia_files::query
    class GetLimitedResult : public QueryResult
    {
        const std::vector<MultimediaFilesRecord> records;
        unsigned int dbRecordsCount;

      public:
        explicit GetLimitedResult(std::vector<MultimediaFilesRecord> records);
        explicit GetLimitedResult(std::vector<MultimediaFilesRecord> records, unsigned int dbRecordsCount);
        [[nodiscard]] auto getResult() const -> std::vector<MultimediaFilesRecord>;

        [[nodiscard]] auto getCount() const noexcept -> unsigned int;
        [[nodiscard]] auto debugInfo() const -> std::string override;
    };



@@ 69,11 70,12 @@ namespace db::multimedia_files::query
    class GetArtistsLimitedResult : public QueryResult
    {
        const std::vector<Artist> records;
        unsigned int dbRecordsCount;

      public:
        explicit GetArtistsLimitedResult(std::vector<Artist> records);
        explicit GetArtistsLimitedResult(std::vector<Artist> records, unsigned int dbRecordsCount);
        [[nodiscard]] auto getResult() const -> std::vector<Artist>;

        [[nodiscard]] auto getCount() const noexcept -> unsigned int;
        [[nodiscard]] auto debugInfo() const -> std::string override;
    };



@@ 90,11 92,12 @@ namespace db::multimedia_files::query
    class GetAlbumsLimitedResult : public QueryResult
    {
        const std::vector<Album> records;
        unsigned int dbRecordsCount;

      public:
        explicit GetAlbumsLimitedResult(std::vector<Album> records);
        explicit GetAlbumsLimitedResult(std::vector<Album> records, unsigned int dbRecordsCount);
        [[nodiscard]] auto getResult() const -> std::vector<Album>;

        [[nodiscard]] auto getCount() const noexcept -> unsigned int;
        [[nodiscard]] auto debugInfo() const -> std::string override;
    };
} // namespace db::multimedia_files::query