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 }