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

0001 // SPDX-FileCopyrightText: 2021 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 "userplaylistmodel.h"
0006 
0007 #include <asyncytmusic.h>
0008 #include <algorithm>
0009 #include <random>
0010 
0011 #include <QRandomGenerator>
0012 
0013 #include <QStringBuilder>
0014 
0015 #include <iostream>
0016 
0017 #include "albummodel.h"
0018 #include "localplaylistmodel.h"
0019 #include "playlistutils.h"
0020 #include "playlistmodel.h"
0021 
0022 namespace ranges = std::ranges;
0023 
0024 UserPlaylistModel::UserPlaylistModel(QObject *parent)
0025     : AbstractYTMusicModel(parent)
0026 {
0027     auto handleResult = [=, this](watch::Playlist &&playlist) {
0028         setLoading(false);
0029 
0030         beginResetModel();
0031         m_playlist = std::move(playlist);
0032         endResetModel();
0033         setCurrentVideoId({});
0034         if (m_shuffle) {
0035             shufflePlaylist();
0036 
0037             // reset shuffle
0038             setShuffle(false);
0039         }
0040         if (!m_playlist.tracks.empty()) {
0041             setCurrentVideoId(QString::fromStdString(m_playlist.tracks.front().video_id));
0042         }
0043     };
0044     connect(this, &UserPlaylistModel::initialVideoIdChanged, this, [=, this] {
0045         if (m_initialVideoId.isEmpty()) {
0046             return;
0047         }
0048 
0049         setLoading(true);
0050         auto future = YTMusicThread::instance()->fetchWatchPlaylist(m_initialVideoId);
0051         QCoro::connect(std::move(future), this, handleResult);
0052     });
0053     connect(this, &UserPlaylistModel::playlistIdChanged, this, [=, this] {
0054         if (m_playlistId.isEmpty()) {
0055             return;
0056         }
0057 
0058         setLoading(true);
0059         auto future = YTMusicThread::instance()->fetchWatchPlaylist(std::nullopt, m_playlistId);
0060         QCoro::connect(std::move(future), this, handleResult);;
0061     });
0062     connect(&YTMusicThread::instance().get(), &AsyncYTMusic::errorOccurred, this, [this] {
0063         setLoading(false);
0064     });
0065     connect(this, &UserPlaylistModel::currentVideoIdChanged, this, [this]() {
0066         Q_EMIT currentIndexChanged();
0067         // Clear lyrics, so we won't display old ones if the next song doesn't have any.
0068         m_lyrics = {};
0069         Q_EMIT lyricsChanged();
0070 
0071         if (!m_currentVideoId.isEmpty()) {
0072             fetchLyrics(m_currentVideoId);
0073         }
0074     });
0075 }
0076 
0077 int UserPlaylistModel::rowCount(const QModelIndex &parent) const
0078 {
0079     return parent.isValid() ? 0 : int(m_playlist.tracks.size());
0080 }
0081 
0082 QVariant UserPlaylistModel::data(const QModelIndex &index, int role) const
0083 {
0084     switch (role) {
0085     case Title:
0086         return QString::fromStdString(m_playlist.tracks[index.row()].title);
0087     case VideoId:
0088         return QString::fromStdString(m_playlist.tracks[index.row()].video_id);
0089     case Artists:
0090         return PlaylistUtils::artistsToString(m_playlist.tracks[index.row()].artists);
0091     case Album:
0092         return QString::fromStdString(m_playlist.tracks[index.row()].album.value_or(meta::Album()).name);
0093     case IsCurrent:
0094         return m_playlist.tracks[index.row()].video_id == m_currentVideoId.toStdString();
0095     }
0096 
0097     Q_UNREACHABLE();
0098 
0099     return {};
0100 }
0101 
0102 QHash<int, QByteArray> UserPlaylistModel::roleNames() const
0103 {
0104     return {
0105         {Title, "title"},
0106         {VideoId, "videoId"},
0107         {Artists, "artists"},
0108         {Album, "album"},
0109         {IsCurrent, "isCurrent"},
0110     };
0111 }
0112 
0113 bool UserPlaylistModel::moveRow(int sourceRow,int destinationRow)
0114 {
0115     if(beginMoveRows(QModelIndex(), sourceRow, sourceRow, QModelIndex(), destinationRow)) {
0116         m_playlist.tracks.insert(m_playlist.tracks.begin()+destinationRow, 1, m_playlist.tracks[sourceRow]);
0117         if(sourceRow < destinationRow) {
0118             m_playlist.tracks.erase(m_playlist.tracks.begin()+sourceRow);
0119         }
0120         else {
0121             m_playlist.tracks.erase(m_playlist.tracks.begin()+sourceRow+1);
0122         }
0123         endMoveRows();
0124 
0125         Q_EMIT currentIndexChanged();
0126         Q_EMIT canSkipChanged();
0127         Q_EMIT canSkipBackChanged();
0128         return true;
0129     }
0130     return false;
0131 }
0132 
0133 QString UserPlaylistModel::initialVideoId() const
0134 {
0135     return m_initialVideoId;
0136 }
0137 
0138 void UserPlaylistModel::setInitialVideoId(const QString &videoId)
0139 {
0140     m_initialVideoId = videoId;
0141     Q_EMIT initialVideoIdChanged();
0142 }
0143 
0144 QString UserPlaylistModel::nextVideoId() const
0145 {
0146     auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
0147                                      [this](const watch::Playlist::Track &track) {
0148         return track.video_id == m_currentVideoId.toStdString();
0149     });
0150 
0151     if (currentTrack == m_playlist.tracks.end() || currentTrack + 1 == m_playlist.tracks.end()) {
0152         return {};
0153     }
0154 
0155     return QString::fromStdString((currentTrack + 1)->video_id);
0156 }
0157 
0158 QString UserPlaylistModel::previousVideoId() const
0159 {
0160     auto currentTrack = std::find_if(m_playlist.tracks.begin(), m_playlist.tracks.end(),
0161                                      [this](const watch::Playlist::Track &track) {
0162         return track.video_id == m_currentVideoId.toStdString();
0163     });
0164 
0165     if (currentTrack == m_playlist.tracks.end() || currentTrack - 1 == m_playlist.tracks.end()) {
0166         return {};
0167     }
0168 
0169     return QString::fromStdString((currentTrack - 1)->video_id);
0170 }
0171 
0172 QString UserPlaylistModel::currentVideoId() const
0173 {
0174     return m_currentVideoId;
0175 }
0176 
0177 void UserPlaylistModel::setCurrentVideoId(const QString &videoId)
0178 {
0179     const auto old = m_currentVideoId;
0180     m_currentVideoId = videoId;
0181     emitCurrentVideoChanged(old);
0182     Q_EMIT currentVideoIdChanged();
0183     Q_EMIT canSkipChanged();
0184     Q_EMIT canSkipBackChanged();
0185 
0186 }
0187 
0188 int UserPlaylistModel::currentIndex() const
0189 {
0190     auto currentTrack = ranges::find_if(m_playlist.tracks,
0191                                         [this](const watch::Playlist::Track &track) {
0192         return track.video_id == m_currentVideoId.toStdString();
0193     });
0194 
0195     return std::distance(m_playlist.tracks.begin(), currentTrack);
0196 }
0197 
0198 bool UserPlaylistModel::canSkip() const
0199 {
0200     const auto currentTrackIt = ranges::find_if(m_playlist.tracks,
0201                                                 [this](const watch::Playlist::Track &track) {
0202         return track.video_id == m_currentVideoId.toStdString();
0203     });
0204 
0205     return currentTrackIt != m_playlist.tracks.end() - 1
0206             && currentTrackIt != m_playlist.tracks.end()
0207             && !m_playlist.tracks.empty();
0208 }
0209 
0210 bool UserPlaylistModel::canSkipBack() const
0211 {
0212     const auto currentTrackIt = std::ranges::find_if(m_playlist.tracks, [this](const watch::Playlist::Track &track) {
0213         return track.video_id == m_currentVideoId.toStdString();
0214     });
0215 
0216     return currentTrackIt != m_playlist.tracks.begin()
0217             && !m_playlist.tracks.empty();
0218 }
0219 
0220 void UserPlaylistModel::next()
0221 {
0222     setCurrentVideoId(nextVideoId());
0223     Q_EMIT currentVideoIdChanged();
0224     Q_EMIT canSkipChanged();
0225     Q_EMIT canSkipBackChanged();
0226 
0227 }
0228 
0229 void UserPlaylistModel::previous()
0230 {
0231     setCurrentVideoId(previousVideoId());
0232     Q_EMIT currentVideoIdChanged();
0233     Q_EMIT canSkipChanged();
0234     Q_EMIT canSkipBackChanged();
0235 
0236 }
0237 
0238 void UserPlaylistModel::skipTo(const QString &videoId)
0239 {
0240     const auto old = m_currentVideoId;
0241     m_currentVideoId = videoId;
0242     emitCurrentVideoChanged(old);
0243     Q_EMIT currentVideoIdChanged();
0244     Q_EMIT canSkipChanged();
0245     Q_EMIT canSkipBackChanged();
0246 
0247 }
0248 
0249 void UserPlaylistModel::playNext(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
0250 {
0251     const auto currentIt = ranges::find_if(m_playlist.tracks,
0252                                         [this](const watch::Playlist::Track &track) {
0253         return track.video_id == m_currentVideoId.toStdString();
0254     });
0255 
0256     watch::Playlist::Track track;
0257     track.video_id = videoId.toStdString();
0258     track.title = title.toStdString();
0259     track.artists = artists;
0260 
0261     if (currentIt == m_playlist.tracks.end() || m_playlist.tracks.empty()) {
0262         beginInsertRows({}, 0, 0);
0263         m_playlist.tracks.push_back(std::move(track));
0264         endInsertRows();
0265         setCurrentVideoId(videoId);
0266         Q_EMIT canSkipChanged();
0267         Q_EMIT canSkipBackChanged();
0268 
0269         return;
0270     }
0271 
0272     if (currentIt == m_playlist.tracks.end() - 1) {
0273         int index = std::distance(m_playlist.tracks.begin(), currentIt + 1);
0274         beginInsertRows({}, index, index);
0275         m_playlist.tracks.push_back(std::move(track));
0276         endInsertRows();
0277         Q_EMIT canSkipChanged();
0278         Q_EMIT canSkipBackChanged();
0279 
0280         return;
0281     }
0282 
0283     int index = std::distance(m_playlist.tracks.begin(), currentIt + 1);
0284     beginInsertRows({}, index, index);
0285     m_playlist.tracks.insert(currentIt + 1, std::move(track));
0286     endInsertRows();
0287     Q_EMIT canSkipChanged();
0288     Q_EMIT canSkipBackChanged();
0289 
0290 }
0291 
0292 void UserPlaylistModel::append(const QString &videoId, const QString &title, const std::vector<meta::Artist> &artists)
0293 {
0294     watch::Playlist::Track track;
0295     track.video_id = videoId.toStdString();
0296     track.title = title.toStdString();
0297     track.artists = artists;
0298 
0299     beginInsertRows({}, m_playlist.tracks.size(), m_playlist.tracks.size());
0300     m_playlist.tracks.push_back(std::move(track));
0301     endInsertRows();
0302 
0303     if (m_playlist.tracks.size() == 1) {
0304         setCurrentVideoId(videoId);
0305     }
0306 
0307     Q_EMIT canSkipChanged();
0308 }
0309 
0310 void UserPlaylistModel::clear()
0311 {
0312     beginResetModel();
0313     m_playlist.tracks.clear();
0314     endResetModel();
0315 
0316     setCurrentVideoId({});
0317     Q_EMIT canSkipChanged();
0318     Q_EMIT canSkipBackChanged();
0319 }
0320 
0321 void UserPlaylistModel::clearExceptCurrent()
0322 {
0323     int index = currentIndex();
0324     Q_ASSERT(checkIndex(createIndex(index, 0), CheckIndexOption::IndexIsValid | CheckIndexOption::DoNotUseParent));
0325     if(m_playlist.tracks.empty()) {return;}
0326     if((unsigned) index < m_playlist.tracks.size() - 1) {
0327         beginRemoveRows({}, index + 1, m_playlist.tracks.size() - 1);
0328         m_playlist.tracks.erase(m_playlist.tracks.begin() + index + 1, m_playlist.tracks.end());
0329         endRemoveRows();
0330     }
0331     
0332     if(index > 0) {
0333         beginRemoveRows({}, 0, index - 1);
0334         m_playlist.tracks.erase(m_playlist.tracks.begin(), m_playlist.tracks.begin() + index);
0335         endRemoveRows();
0336     }
0337     
0338     Q_EMIT canSkipChanged();
0339     Q_EMIT canSkipBackChanged();
0340 }
0341 
0342 
0343 void UserPlaylistModel::remove(const QString &videoId)
0344 {
0345     if (m_currentVideoId == videoId) {
0346         setCurrentVideoId(nextVideoId());
0347     }
0348 
0349     const auto trackIt = ranges::find_if(m_playlist.tracks, [&](const watch::Playlist::Track &track) {
0350         return track.video_id == videoId.toStdString();
0351     });
0352     int index = std::distance(m_playlist.tracks.begin(), trackIt);
0353     beginRemoveRows({}, index, index);
0354     m_playlist.tracks.erase(trackIt);
0355     endRemoveRows();
0356 
0357     Q_EMIT canSkipChanged();
0358     Q_EMIT canSkipBackChanged();
0359 }
0360 
0361 void UserPlaylistModel::shufflePlaylist()
0362 {
0363     // Only shuffle playlist after current track
0364     if (!m_currentVideoId.isEmpty()) {
0365         const auto currentIt = ranges::find_if(m_playlist.tracks,
0366                                                [this](const watch::Playlist::Track &track) {
0367             return track.video_id == m_currentVideoId.toStdString();
0368         });
0369 
0370         std::shuffle(currentIt + 1, m_playlist.tracks.end(), *QRandomGenerator::global());
0371     } else {
0372         ranges::shuffle(m_playlist.tracks, *QRandomGenerator::global());
0373     }
0374     Q_EMIT dataChanged(index(0), index(m_playlist.tracks.size() - 1), {});
0375 }
0376 
0377 void UserPlaylistModel::appendPlaylist(PlaylistModel *playlistModel)
0378 {
0379     for (const auto &track : playlistModel->playlist().tracks) {
0380         if (track.video_id) {
0381             append(QString::fromStdString(*track.video_id), QString::fromStdString(track.title), track.artists);
0382         }
0383     }
0384 }
0385 
0386 void UserPlaylistModel::appendAlbum(AlbumModel *albumModel)
0387 {
0388     for (const auto &track : albumModel->album().tracks) {
0389         if (track.video_id) {
0390             append(QString::fromStdString(*track.video_id), QString::fromStdString(track.title), track.artists);
0391         }
0392     }
0393 }
0394 
0395 void UserPlaylistModel::playFavourites(FavouritesModel *favouriteModel, bool shuffled)
0396 {
0397     clear();
0398     appendFavourites(favouriteModel, shuffled);
0399 
0400 }
0401 
0402 void UserPlaylistModel::appendFavourites(FavouritesModel *favouriteModel, bool shuffled)
0403 {
0404     std::vector<Song> favourites(favouriteModel->getFavouriteSongs());
0405     if(shuffled) {
0406         ranges::shuffle(favourites, *QRandomGenerator::global());
0407     }
0408     ranges::for_each(favourites, [this](const Song &song) {
0409         meta::Artist artist;
0410         artist.name = song.artist.toStdString();
0411         append(song.videoId, song.title, std::vector<meta::Artist>({artist}));
0412     });
0413 }
0414 
0415 void UserPlaylistModel::playPlaybackHistory(PlaybackHistoryModel *playbackHistory, bool shuffled)
0416 {
0417     clear();
0418     appendPlaybackHistory(playbackHistory, shuffled);
0419 
0420 }
0421 
0422 void UserPlaylistModel::appendPlaybackHistory(PlaybackHistoryModel *playbackHistory, bool shuffled)
0423 {
0424     std::vector<PlayedSong> playedSongs(playbackHistory->getPlayedSong());
0425     if(shuffled) {
0426         std::shuffle(playedSongs.begin(), playedSongs.end(), *QRandomGenerator::global());
0427     }
0428     ranges::for_each(playedSongs, [this](const PlayedSong &song) {
0429         meta::Artist artist;
0430         artist.name = song.artist.toStdString();
0431         append(song.videoId, song.title, std::vector<meta::Artist>({artist}));
0432     });
0433 }
0434 
0435 void UserPlaylistModel::playLocalPlaylist(LocalPlaylistModel *playlistModel, bool shuffled)
0436 {
0437     clear();
0438     appendLocalPlaylist(playlistModel, shuffled);
0439 }
0440 
0441 void UserPlaylistModel::appendLocalPlaylist(LocalPlaylistModel *playlistModel, bool shuffled)
0442 {
0443     std::vector<PlaylistEntry> entries = playlistModel->entries();
0444     if (shuffled) {
0445         ranges::shuffle(entries, *QRandomGenerator::global());
0446     }
0447     for (const auto &entry : entries) {
0448         meta::Artist artist;
0449         artist.name = entry.artists.toStdString();
0450         append(entry.videoId, entry.title, std::vector<meta::Artist>({artist}));
0451     }
0452 }
0453 
0454 void UserPlaylistModel::emitCurrentVideoChanged(const QString &oldVideoId)
0455 {
0456 
0457     const auto oldVideoIt = ranges::find_if(m_playlist.tracks,
0458                                             [=](const watch::Playlist::Track &track) {
0459         return track.video_id == oldVideoId.toStdString();
0460     });
0461     const auto currentVideoIt = ranges::find_if(m_playlist.tracks,
0462                                                 [this](const watch::Playlist::Track &track) {
0463         return track.video_id == m_currentVideoId.toStdString();
0464     });
0465 
0466     int oldIndex = std::distance(m_playlist.tracks.begin(), oldVideoIt);
0467     int newIndex = std::distance(m_playlist.tracks.begin(), currentVideoIt);
0468 
0469     Q_EMIT dataChanged(index(oldIndex), index(oldIndex), {IsCurrent});
0470     Q_EMIT dataChanged(index(newIndex), index(newIndex), {IsCurrent});
0471 }
0472 
0473 void UserPlaylistModel::fetchLyrics(const QString &videoId)
0474 {
0475     auto future = YTMusicThread::instance()->fetchWatchPlaylist(videoId);
0476     QCoro::connect(std::move(future), this, [=, this](const auto &playlist) {
0477         if (playlist.lyrics) {
0478             QCoro::connect(YTMusicThread::instance()->fetchLyrics(QString::fromStdString(*playlist.lyrics)), this, [=, this](const auto &lyrics) {
0479                 m_lyrics = lyrics;
0480                 Q_EMIT lyricsChanged();
0481             });
0482         } else {
0483             Q_EMIT noLyrics();
0484         }
0485     });
0486 }
0487 
0488 bool UserPlaylistModel::shuffle() const
0489 {
0490     return m_shuffle;
0491 }
0492 
0493 QString UserPlaylistModel::lyrics() const {
0494     return QString::fromStdString(m_lyrics.lyrics);
0495 }
0496 
0497 void UserPlaylistModel::setShuffle(bool shuffle)
0498 {
0499     m_shuffle = shuffle;
0500     Q_EMIT shuffleChanged();
0501 }
0502 
0503 QString UserPlaylistModel::playlistId() const
0504 {
0505     return m_playlistId;
0506 }
0507 
0508 void UserPlaylistModel::setPlaylistId(const QString &playlistId)
0509 {
0510     m_playlistId = playlistId;
0511     Q_EMIT playlistIdChanged();
0512 }
0513 
0514 QUrl UserPlaylistModel::webUrl() const
0515 {
0516     return QUrl(YTMUSIC_WEB_BASE_URL % "watch?v=" % m_currentVideoId);
0517 }
0518