File indexing completed on 2022-08-04 15:34:39

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