File indexing completed on 2024-05-12 16:21:16

0001 // SPDX-FileCopyrightText: 2022 Jonah Brüchert <jbb@kaidan.im>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 #include "library.h"
0006 
0007 #include <QStandardPaths>
0008 #include <QDir>
0009 #include <QStringBuilder>
0010 #include <QGuiApplication>
0011 
0012 #include <ThreadedDatabase>
0013 
0014 namespace ranges = std::ranges;
0015 
0016 Library::Library(QObject *parent)
0017     : QObject{parent}
0018     , m_database(ThreadedDatabase::establishConnection([]() -> DatabaseConfiguration {
0019         const auto databaseDirectory = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
0020         // Make sure the database directory exists
0021         QDir(databaseDirectory).mkpath(QStringLiteral("."));
0022 
0023         DatabaseConfiguration config;
0024         config.setDatabaseName(databaseDirectory % QDir::separator() % "library.sqlite");
0025         config.setType(DatabaseType::SQLite);
0026         return config;
0027     }()))
0028 {
0029     m_database->runMigrations(":/migrations/");
0030     m_searches = new SearchHistoryModel(this);
0031 
0032     refreshFavourites();
0033     refreshPlaybackHistory();
0034 }
0035 
0036 Library::~Library() = default;
0037 
0038 Library &Library::instance()
0039 {
0040     static Library inst;
0041     return inst;
0042 }
0043 
0044 FavouritesModel *Library::favourites()
0045 {
0046     return m_favourites;
0047 }
0048 
0049 void Library::addFavourite(const QString &videoId, const QString &title, const QString &artist, const QString &album)
0050 {
0051     QCoro::connect(addSong(videoId, title, artist, album), this, [=, this] {
0052         QCoro::connect(m_database->execute("insert or ignore into favourites (video_id) values (?)", videoId),
0053                        this, &Library::refreshFavourites);
0054     });
0055 }
0056 
0057 void Library::removeFavourite(const QString &videoId)
0058 {
0059     QCoro::connect(m_database->execute("delete from favourites where video_id = ?", videoId),
0060                    this, &Library::refreshFavourites);
0061 }
0062 
0063 FavouriteWatcher *Library::favouriteWatcher(const QString &videoId)
0064 {
0065     if (videoId.isEmpty()) {
0066         return nullptr;
0067     }
0068     return new FavouriteWatcher(this, videoId);
0069 }
0070 
0071 SearchHistoryModel *Library::searches()
0072 {
0073     return m_searches;
0074 }
0075 
0076 void Library::addSearch(const QString &text)
0077 {
0078     m_searches->addSearch(text);
0079     QCoro::connect(m_database->execute("insert into searches (search_query) values (?)", text), this, &Library::searchesChanged);
0080 }
0081 
0082 void Library::removeSearch(const QString &text) {
0083     m_searches->removeSearch(text);
0084     QCoro::connect(m_database->execute("delete from searches where search_query = ?", text), this, &Library::searchesChanged);
0085 }
0086 
0087 const QString& Library::temporarySearch()
0088 {
0089     return m_searches->temporarySearch();
0090 }
0091 
0092 void Library::setTemporarySearch(const QString& text)
0093 {
0094     m_searches->setTemporarySearch(text);
0095     Q_EMIT temporarySearchChanged();
0096 }
0097 
0098 
0099 PlaybackHistoryModel *Library::playbackHistory()
0100 {
0101     return m_playbackHistory;
0102 }
0103 
0104 void Library::refreshPlaybackHistory()
0105 {
0106     // playbackHistory
0107     auto future = m_database->getResults<PlayedSong>(
0108         "select * from played_songs natural join songs");
0109     m_playbackHistory = new PlaybackHistoryModel(std::move(future), this);
0110 
0111     // mostPlayed
0112     auto future2 = m_database->getResults<PlayedSong>(
0113         "select * from played_songs natural join songs order by plays desc limit 10");
0114     m_mostPlayed = new PlaybackHistoryModel(std::move(future2), this);
0115     Q_EMIT playbackHistoryChanged();
0116 }
0117 
0118 void Library::refreshFavourites()
0119 {
0120     auto future = m_database->getResults<Song>(
0121         "select * from favourites natural join songs order by favourites.rowid desc");
0122     m_favourites = new FavouritesModel(std::move(future), this);
0123     Q_EMIT favouritesChanged();
0124 }
0125 
0126 void Library::addPlaybackHistoryItem(const QString &videoId, const QString &title, const QString &artist, const QString &album)
0127 {
0128     QCoro::connect(addSong(videoId, title, artist, album), this, [=, this] {
0129         QCoro::connect(m_database->execute("insert or ignore into played_songs (video_id, plays) values (?, ?)", videoId, 0), this, [=, this] {
0130             QCoro::connect(m_database->execute("update played_songs set plays = plays + 1 where video_id = ? ", videoId),
0131                            this, &Library::refreshPlaybackHistory);
0132         });
0133     });
0134 }
0135 void Library::removePlaybackHistoryItem(const QString &videoId)
0136 {
0137     QCoro::connect(m_database->execute("delete from played_songs where video_id = ?", videoId),
0138                    this, &Library::refreshPlaybackHistory);
0139 }
0140 
0141 WasPlayedWatcher *Library::wasPlayedWatcher(const QString& videoId)
0142 {
0143     if(videoId.isEmpty()){
0144         return nullptr;
0145     }
0146     return new WasPlayedWatcher(this, videoId);
0147 }
0148 
0149 
0150 
0151 
0152 PlaybackHistoryModel *Library::mostPlayed()
0153 {
0154     return m_mostPlayed;
0155 }
0156 
0157 QNetworkAccessManager &Library::nam()
0158 {
0159     return m_networkImageCacher;
0160 }
0161 
0162 QFuture<void> Library::addSong(const QString &videoId, const QString &title, const QString &artist, const QString &album)
0163 {
0164     // replace is used here to update songs from times when we didn't store artist and album
0165     return m_database->execute("insert or replace into songs (video_id, title, artist, album) values (?, ?, ?, ?)", videoId, title, artist, album);
0166 }
0167 
0168 PlaybackHistoryModel::PlaybackHistoryModel(QFuture<std::vector<PlayedSong>> &&songs, QObject *parent)
0169     : QAbstractListModel(parent)
0170 {
0171     QCoro::connect(std::move(songs), this, [this](const auto songs) {
0172         beginResetModel();
0173         m_playedSongs = songs;
0174         endResetModel();
0175     });
0176 }
0177 
0178 PlaybackHistoryModel::PlaybackHistoryModel(QObject *parent)
0179     : QAbstractListModel(parent)
0180 {
0181 }
0182 
0183 QHash<int, QByteArray> PlaybackHistoryModel::roleNames() const {
0184     return {
0185         {Roles::VideoId, "videoId"},
0186         {Roles::Title, "title"},
0187         {Roles::Artists, "artists"},
0188         {Roles::ArtistsDisplayString, "artistsDisplayString"},
0189         {Roles::Plays, "plays"}
0190     };
0191 }
0192 
0193 int PlaybackHistoryModel::rowCount(const QModelIndex &parent) const {
0194     return parent.isValid() ? 0 : m_playedSongs.size();
0195 }
0196 
0197 QVariant PlaybackHistoryModel::data(const QModelIndex &index, int role) const {
0198     switch (role) {
0199     case Roles::VideoId:
0200         return m_playedSongs.at(index.row()).videoId;
0201     case Roles::Title:
0202         return m_playedSongs.at(index.row()).title;
0203     case Roles::Artists:
0204         return QVariant::fromValue(std::vector<meta::Artist> {
0205             {
0206                 m_playedSongs.at(index.row()).artist.toStdString(),
0207                 {}
0208             }
0209         });
0210     case Roles::ArtistsDisplayString:
0211         return m_playedSongs.at(index.row()).artist;
0212     case Roles::Plays:
0213         return m_playedSongs.at(index.row()).plays;
0214     }
0215 
0216     Q_UNREACHABLE();
0217 }
0218 
0219 std::vector<PlayedSong> PlaybackHistoryModel::getPlayedSong() const
0220 {
0221     return m_playedSongs;
0222 }
0223 
0224 
0225 FavouritesModel::FavouritesModel(QFuture<std::vector<Song>> &&songs, QObject *parent)
0226     : QAbstractListModel(parent)
0227 {
0228     QCoro::connect(std::move(songs), this, [this](const auto songs) {
0229         beginResetModel();
0230         m_favouriteSongs = songs;
0231         endResetModel();
0232     });
0233 }
0234 
0235 QHash<int, QByteArray> FavouritesModel::roleNames() const {
0236     return {
0237         {Roles::VideoId, "videoId"},
0238         {Roles::Title, "title"},
0239         {Roles::Artists, "artists"},
0240         {Roles::ArtistsDisplayString, "artistsDisplayString"}
0241     };
0242 }
0243 
0244 int FavouritesModel::rowCount(const QModelIndex &parent) const {
0245     return parent.isValid() ? 0 : m_favouriteSongs.size();
0246 }
0247 
0248 QVariant FavouritesModel::data(const QModelIndex &index, int role) const {
0249     switch (role) {
0250     case Roles::VideoId:
0251         return m_favouriteSongs.at(index.row()).videoId;
0252     case Roles::Title:
0253         return m_favouriteSongs.at(index.row()).title;
0254     case Roles::ArtistsDisplayString:
0255         return m_favouriteSongs.at(index.row()).artist;
0256     case Roles::Artists:
0257         return QVariant::fromValue(std::vector<meta::Artist> {
0258             {
0259                 m_favouriteSongs.at(index.row()).artist.toStdString(),
0260                 {}
0261             }
0262         });
0263     }
0264 
0265     Q_UNREACHABLE();
0266 }
0267 
0268 std::vector<Song> FavouritesModel::getFavouriteSongs() const {
0269     return m_favouriteSongs;
0270 }
0271 
0272 FavouriteWatcher::FavouriteWatcher(Library *library, const QString &videoId)
0273     : QObject(library), m_videoId(videoId), m_library(library)
0274 {
0275     auto update = [this] {
0276         QCoro::connect(m_library->database().getResult<SingleValue<bool>>("select count(*) > 0 from favourites where video_id = ?", m_videoId), this, [this](auto count) {
0277             if (count) {
0278                 m_isFavourite = count->value;
0279                 Q_EMIT isFavouriteChanged();
0280             }
0281         });
0282     };
0283     update();
0284     connect(library, &Library::favouritesChanged, this, update);
0285 }
0286 
0287 bool FavouriteWatcher::isFavourite() const {
0288     return m_isFavourite;
0289 }
0290 
0291 SearchHistoryModel::SearchHistoryModel(Library *library)
0292     : QAbstractListModel(library)
0293 {
0294     auto historyFuture = library->database()
0295                             .getResults<SingleValue<QString>>("select distinct (search_query) from searches order by search_id desc limit 20");
0296 
0297     connect(this, &SearchHistoryModel::filterChanged, this, [library, this]() {
0298         auto future = library->database()
0299             .getResults<SingleValue<QString>>("select distinct (search_query) from searches "
0300                                               "where search_query like '%" % m_filter % "%'"
0301                                               "order by search_id desc limit 20");
0302 
0303         QCoro::connect(std::move(future), this, [this](auto history) {
0304             beginResetModel();
0305             m_history = history;
0306             endResetModel();
0307         });
0308     });
0309 
0310     QCoro::connect(std::move(historyFuture), this, [this](const auto history) {
0311         beginResetModel();
0312         m_history = history;
0313         endResetModel();
0314     });
0315 }
0316 
0317 QHash<int, QByteArray> SearchHistoryModel::roleNames() const
0318 {
0319     return {
0320         { Qt::DisplayRole, "searchQuery" }
0321     };
0322 }
0323 
0324 int SearchHistoryModel::rowCount(const QModelIndex &parent) const {
0325     if(parent.isValid()) {
0326         return 0;
0327     }
0328     else if (temporarySearch().isEmpty()) {
0329         return m_history.size();
0330     }
0331     else {
0332         return m_history.size() + 1;
0333     }
0334 }
0335 
0336 void SearchHistoryModel::removeSearch(const QString &search) {
0337     int row = getRow(search);
0338     if(m_temporarySearch != "") {
0339         beginRemoveRows({}, row+1, row+1);
0340     }
0341     else {
0342         beginRemoveRows({}, row, row);
0343     }
0344     m_history.erase(m_history.begin() + row);
0345     endRemoveRows();
0346 }
0347 
0348 size_t SearchHistoryModel::getRow(const QString &search) const {
0349     auto itr = find_if(m_history.begin(), m_history.end(), [&](const auto &checkedValue) {
0350         return checkedValue.value == search;
0351     });
0352     size_t i = std::distance(m_history.begin(), itr);
0353     Q_ASSERT(i < m_history.size());
0354     return i;
0355 }
0356 
0357 QVariant SearchHistoryModel::data(const QModelIndex &index, int role) const {
0358     switch (role) {
0359         case Qt::DisplayRole:
0360             if(m_temporarySearch == "") {
0361                 return m_history[index.row()].value;
0362             }
0363             else if(index.row() == 0) {
0364                 return m_temporarySearch;
0365             }
0366             else{
0367                 return m_history[index.row() - 1].value;
0368             }
0369     }
0370     
0371     Q_UNREACHABLE();
0372 }
0373 
0374 void SearchHistoryModel::addSearch(const QString& search) {
0375     auto itr = find_if(m_history.begin(), m_history.end(), [&](const auto &checkedValue) {
0376         return checkedValue.value == search;
0377     });
0378     if(itr == m_history.end()) {
0379         beginInsertRows({}, 0, 0);
0380         m_history.insert(m_history.begin(), {search});
0381         endInsertRows();
0382     }
0383 }
0384 
0385 const QString& SearchHistoryModel::temporarySearch() const
0386 {
0387     return m_temporarySearch;
0388 }
0389 
0390 void SearchHistoryModel::setTemporarySearch(const QString& text)
0391 {
0392     if(text == "" && m_temporarySearch != "") {
0393         beginRemoveRows(QModelIndex(), 0, 0);
0394         m_temporarySearch = text;
0395         endRemoveRows();
0396     }
0397     else if(text != "" && m_temporarySearch == "") {
0398         beginInsertRows(QModelIndex(), 0, 0);
0399         m_temporarySearch = text;
0400         endInsertRows();
0401     }
0402     else if(m_temporarySearch != "") {
0403         m_temporarySearch = text;
0404         Q_EMIT dataChanged(createIndex(0,0), createIndex(0,0));
0405     }
0406 }
0407 
0408 
0409 
0410 
0411 WasPlayedWatcher::WasPlayedWatcher(Library* library, const QString& videoId)
0412     : QObject(library), m_videoId(videoId), m_library(library)
0413 {
0414     connect(m_library, &Library::playbackHistoryChanged, this, &WasPlayedWatcher::query);
0415     query();
0416 }
0417 
0418 void WasPlayedWatcher::query()
0419 {
0420     QCoro::connect(m_library->database().getResult<SingleValue<bool>>("select count(*) > 0 from played_songs where video_id = ?", m_videoId), this, &WasPlayedWatcher::update);
0421 }
0422 
0423 
0424 bool WasPlayedWatcher::wasPlayed() const
0425 {
0426     return m_wasPlayed;
0427 }
0428 
0429 
0430 void WasPlayedWatcher::update(std::optional<SingleValue<bool> > result)
0431 {
0432     if(result.has_value())
0433     {
0434         m_wasPlayed = result->value;
0435         Q_EMIT wasPlayedChanged();
0436     }
0437 }
0438 
0439 LocalSearchModel::LocalSearchModel(QObject *parent) : PlaybackHistoryModel(parent)
0440 {
0441     connect(this, &LocalSearchModel::searchQueryChanged, this, [this]() {
0442         auto resultFuture = Library::instance().database()
0443                                 .getResults<PlayedSong>("select * from played_songs natural join songs "
0444                                                         "where title like '%" % m_searchQuery % "%' "
0445                                                         "order by plays desc limit 10");
0446         QCoro::connect(std::move(resultFuture), this, [this](auto results) {
0447                beginResetModel();
0448                m_playedSongs = results;
0449                endResetModel();
0450            });
0451     });
0452 }