File indexing completed on 2023-05-30 11:30:44

0001 /**
0002  * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
0003  * Copyright (C) 2008, 2013 Michael Pyne <mpyne@kde.org>
0004  *
0005  * This program is free software; you can redistribute it and/or modify it under
0006  * the terms of the GNU General Public License as published by the Free Software
0007  * Foundation; either version 2 of the License, or (at your option) any later
0008  * version.
0009  *
0010  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0011  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0012  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0013  *
0014  * You should have received a copy of the GNU General Public License along with
0015  * this program.  If not, see <http://www.gnu.org/licenses/>.
0016  */
0017 
0018 #include "cache.h"
0019 #include "juk-exception.h"
0020 
0021 #include <kmessagebox.h>
0022 #include <kconfig.h>
0023 #include <ktoggleaction.h>
0024 #include <KLocalizedString>
0025 
0026 #include <QDir>
0027 #include <QSaveFile>
0028 
0029 #include "juktag.h"
0030 #include "searchplaylist.h"
0031 #include "historyplaylist.h"
0032 #include "upcomingplaylist.h"
0033 #include "folderplaylist.h"
0034 #include "playlistcollection.h"
0035 #include "actioncollection.h"
0036 #include "juk.h"
0037 #include "juk_debug.h"
0038 
0039 using namespace ActionCollection;
0040 
0041 const int Cache::playlistListCacheVersion = 3;
0042 const int Cache::playlistItemsCacheVersion = 2;
0043 
0044 enum PlaylistType
0045 {
0046     Normal   = 0,
0047     Search   = 1,
0048     History  = 2,
0049     Upcoming = 3,
0050     Folder   = 4
0051 };
0052 
0053 ////////////////////////////////////////////////////////////////////////////////
0054 // public methods
0055 ////////////////////////////////////////////////////////////////////////////////
0056 
0057 Cache *Cache::instance()
0058 {
0059     static Cache cache;
0060     return &cache;
0061 }
0062 
0063 static void parsePlaylistStream(QDataStream &s, PlaylistCollection *collection)
0064 {
0065     while(!s.atEnd()) {
0066         qint32 playlistType;
0067         s >> playlistType;
0068 
0069         Playlist *playlist = nullptr;
0070 
0071         switch(playlistType) {
0072         case Search:
0073         {
0074             SearchPlaylist *p = new SearchPlaylist(collection, *(new PlaylistSearch(JuK::JuKInstance())));
0075             s >> *p;
0076             playlist = p;
0077             break;
0078         }
0079         case History:
0080         {
0081             action<KToggleAction>("showHistory")->setChecked(true);
0082             collection->setHistoryPlaylistEnabled(true);
0083             s >> *collection->historyPlaylist();
0084             playlist = collection->historyPlaylist();
0085             break;
0086         }
0087         case Upcoming:
0088         {
0089             collection->setUpcomingPlaylistEnabled(true);
0090             UpcomingPlaylist *p = collection->upcomingPlaylist();
0091             action<KToggleAction>("saveUpcomingTracks")->setChecked(true);
0092             s >> *p;
0093             playlist = p;
0094             break;
0095         }
0096         case Folder:
0097         {
0098             FolderPlaylist *p = new FolderPlaylist(collection);
0099             s >> *p;
0100             playlist = p;
0101             break;
0102         }
0103         case Normal:
0104         {
0105             Playlist *p = new Playlist(collection, true);
0106             s >> *p;
0107 
0108             // We may have already read this playlist from the folder
0109             // scanner, if an .m3u playlist
0110             if(collection->containsPlaylistFile(p->fileName())) {
0111                 delete p;
0112                 p = nullptr;
0113             }
0114 
0115             playlist = p;
0116             break;
0117         }
0118         default:
0119             qCWarning(JUK_LOG) << "Unknown playlist type" << playlistType << "loading saved playlists";
0120             throw BICStreamException();
0121         } // switch
0122 
0123         qint32 sortColumn;
0124         s >> sortColumn;
0125         if(playlist)
0126             playlist->sortByColumn(sortColumn);
0127     }
0128 }
0129 
0130 void Cache::loadPlaylists(PlaylistCollection *collection) // static
0131 {
0132     const QString playlistsFile = playlistsCacheFileName();
0133     QFile f(playlistsFile);
0134 
0135     if(!f.open(QIODevice::ReadOnly))
0136         return;
0137 
0138     QDataStream fs(&f);
0139 
0140     qint32 version;
0141     fs >> version;
0142 
0143     if(version != 3 || fs.status() != QDataStream::Ok) {
0144         // Either the file is corrupt or is from a truly ancient version
0145         // of JuK.
0146         qCWarning(JUK_LOG) << "Found the playlist cache but it was clearly corrupt.";
0147         return;
0148     }
0149 
0150     // Our checksum is only for the values after the version and checksum so
0151     // we want to get a byte array with just the checksummed data.
0152 
0153     QByteArray data;
0154     quint16 checksum;
0155     fs >> checksum >> data;
0156 
0157     if(fs.status() != QDataStream::Ok || checksum != qChecksum(data.data(), data.size()))
0158         return;
0159 
0160     QDataStream s(&data, QIODevice::ReadOnly);
0161     s.setVersion(QDataStream::Qt_4_3);
0162 
0163     try { // Loading failures are indicated by an exception
0164         parsePlaylistStream(s, collection);
0165     }
0166     catch(BICStreamException &) {
0167         qCCritical(JUK_LOG) << "Exception loading playlists - binary incompatible stream.";
0168         // TODO Restructure the Playlist data model and PlaylistCollection data model
0169         // to be separate from the view/controllers.
0170         return;
0171     }
0172 }
0173 
0174 void Cache::savePlaylists(const PlaylistList &playlists)
0175 {
0176     QString playlistsFile = playlistsCacheFileName();
0177     QSaveFile f(playlistsFile);
0178 
0179     if(!f.open(QIODevice::WriteOnly)) {
0180         qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString();
0181         return;
0182     }
0183 
0184     QByteArray data;
0185     QDataStream s(&data, QIODevice::WriteOnly);
0186     s.setVersion(QDataStream::Qt_4_3);
0187 
0188     for(const auto &it : playlists) {
0189         if(!(it)) {
0190             continue;
0191         }
0192         // TODO back serialization type into Playlist itself
0193         if(dynamic_cast<HistoryPlaylist *>(it)) {
0194             s << qint32(History)
0195                 << *static_cast<HistoryPlaylist *>(it);
0196         }
0197         else if(dynamic_cast<SearchPlaylist *>(it)) {
0198             s << qint32(Search)
0199                 << *static_cast<SearchPlaylist *>(it);
0200         }
0201         else if(dynamic_cast<UpcomingPlaylist *>(it)) {
0202             if(!action<KToggleAction>("saveUpcomingTracks")->isChecked())
0203                 continue;
0204             s << qint32(Upcoming)
0205                 << *static_cast<UpcomingPlaylist *>(it);
0206         }
0207         else if(dynamic_cast<FolderPlaylist *>(it)) {
0208             s << qint32(Folder)
0209                 << *static_cast<FolderPlaylist *>(it);
0210         }
0211         else {
0212             s << qint32(Normal)
0213                 << *(it);
0214         }
0215         s << qint32(it->sortColumn());
0216     }
0217 
0218     QDataStream fs(&f);
0219     fs << qint32(playlistListCacheVersion);
0220     fs << qChecksum(data.data(), data.size());
0221 
0222     fs << data;
0223 
0224     if(!f.commit())
0225         qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString();
0226 }
0227 
0228 void Cache::ensureAppDataStorageExists() // static
0229 {
0230     QString dirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
0231     QDir appDataDir(dirPath);
0232 
0233     if(!appDataDir.exists() && !appDataDir.mkpath(dirPath))
0234         qCCritical(JUK_LOG) << "Unable to create appdata storage in" << dirPath;
0235 }
0236 
0237 bool Cache::cacheFileExists() // static
0238 {
0239     return QFile::exists(fileHandleCacheFileName());
0240 }
0241 
0242 // Despite the 'Cache' class name, these data files are not regenerable and so
0243 // should not be stored in cache directory.
0244 QString Cache::fileHandleCacheFileName() // static
0245 {
0246     return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/cache";
0247 }
0248 
0249 QString Cache::playlistsCacheFileName() // static
0250 {
0251     return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/playlists";
0252 }
0253 
0254 ////////////////////////////////////////////////////////////////////////////////
0255 // private methods
0256 ////////////////////////////////////////////////////////////////////////////////
0257 
0258 Cache::Cache()
0259 {
0260 
0261 }
0262 
0263 bool Cache::prepareToLoadCachedItems()
0264 {
0265     m_loadFile.setFileName(fileHandleCacheFileName());
0266     if(!m_loadFile.open(QIODevice::ReadOnly))
0267         return false;
0268 
0269     m_loadDataStream.setDevice(&m_loadFile);
0270 
0271     int dataStreamVersion = CacheDataStream::Qt_3_3;
0272 
0273     qint32 version;
0274     m_loadDataStream >> version;
0275 
0276     switch(version) {
0277     case 2:
0278         dataStreamVersion = CacheDataStream::Qt_4_3;
0279         Q_FALLTHROUGH();
0280 
0281         // Other than that we're compatible with cache v1, so fallthrough
0282         // to setCacheVersion
0283 
0284     case 1: {
0285         m_loadDataStream.setCacheVersion(1);
0286         m_loadDataStream.setVersion(dataStreamVersion);
0287 
0288         qint32 checksum;
0289         m_loadDataStream >> checksum
0290           >> m_loadFileBuffer.buffer();
0291 
0292         m_loadFileBuffer.open(QIODevice::ReadOnly);
0293         m_loadDataStream.setDevice(&m_loadFileBuffer);
0294 
0295         qint32 checksumExpected = qChecksum(
0296                 m_loadFileBuffer.data(), m_loadFileBuffer.size());
0297         if(m_loadDataStream.status() != CacheDataStream::Ok ||
0298                 checksum != checksumExpected)
0299         {
0300             qCCritical(JUK_LOG) << "Music cache checksum expected to get" << checksumExpected <<
0301                         "actually was" << checksum;
0302             KMessageBox::error(0, i18n("The music data cache has been corrupted. JuK "
0303                                        "needs to rescan it now. This may take some time."));
0304             return false;
0305         }
0306 
0307         break;
0308     }
0309     default: {
0310         m_loadDataStream.device()->reset();
0311         m_loadDataStream.setCacheVersion(0);
0312 
0313         // This cache is so old that this is just a wild guess here that 3.3
0314         // is compatible.
0315         m_loadDataStream.setVersion(CacheDataStream::Qt_3_3);
0316         break;
0317     }
0318     }
0319 
0320     return true;
0321 }
0322 
0323 FileHandle Cache::loadNextCachedItem()
0324 {
0325     if(!m_loadFile.isOpen() || !m_loadDataStream.device()) {
0326         qCWarning(JUK_LOG) << "Already completed reading cache file.";
0327         return FileHandle();
0328     }
0329 
0330     if(m_loadDataStream.status() == QDataStream::ReadCorruptData) {
0331         qCCritical(JUK_LOG) << "Attempted to read file handle from corrupt cache file.";
0332         return FileHandle();
0333     }
0334 
0335     if(!m_loadDataStream.atEnd()) {
0336         QString fileName;
0337         m_loadDataStream >> fileName;
0338         fileName.squeeze();
0339 
0340         return FileHandle(fileName, m_loadDataStream);
0341     }
0342     else {
0343         m_loadDataStream.setDevice(0);
0344         m_loadFile.close();
0345 
0346         return FileHandle();
0347     }
0348 }
0349 
0350 // vim: set et sw=4 tw=0 sta: