cache.cpp 9.84 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**
 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
 * Copyright (C) 2008, 2013 Michael Pyne <mpyne@kde.org>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
17

18
#include "cache.h"
19
#include "juk-exception.h"
20

21 22
#include <kmessagebox.h>
#include <kconfig.h>
Tim Beaulen's avatar
Tim Beaulen committed
23
#include <ktoggleaction.h>
Michael Pyne's avatar
Michael Pyne committed
24
#include <KLocalizedString>
25

26
#include <QDir>
Tim Beaulen's avatar
Tim Beaulen committed
27
#include <QBuffer>
28
#include <QtGlobal>
Michael Pyne's avatar
Michael Pyne committed
29
#include <QSaveFile>
Tim Beaulen's avatar
Tim Beaulen committed
30

31
#include "juktag.h"
32 33
#include "searchplaylist.h"
#include "historyplaylist.h"
34
#include "upcomingplaylist.h"
35
#include "folderplaylist.h"
36 37
#include "playlistcollection.h"
#include "actioncollection.h"
Michael Pyne's avatar
Michael Pyne committed
38 39
#include "juk.h"
#include "juk_debug.h"
40 41

using namespace ActionCollection;
42

43 44
const int Cache::playlistListCacheVersion = 3;
const int Cache::playlistItemsCacheVersion = 2;
45 46 47 48 49 50 51 52 53

enum PlaylistType
{
    Normal   = 0,
    Search   = 1,
    History  = 2,
    Upcoming = 3,
    Folder   = 4
};
54

55
////////////////////////////////////////////////////////////////////////////////
56
// public methods
57 58
////////////////////////////////////////////////////////////////////////////////

59
Cache *Cache::instance()
60
{
61 62
    static Cache cache;
    return &cache;
63 64
}

65
static void parsePlaylistStream(QDataStream &s, PlaylistCollection *collection)
66
{
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
    while(!s.atEnd()) {
        qint32 playlistType;
        s >> playlistType;

        Playlist *playlist = nullptr;

        switch(playlistType) {
        case Search:
        {
            SearchPlaylist *p = new SearchPlaylist(collection);
            s >> *p;
            playlist = p;
            break;
        }
        case History:
        {
            action<KToggleAction>("showHistory")->setChecked(true);
            collection->setHistoryPlaylistEnabled(true);
            s >> *collection->historyPlaylist();
            playlist = collection->historyPlaylist();
            break;
        }
        case Upcoming:
        {
            /*
            collection->setUpcomingPlaylistEnabled(true);
            Playlist *p = collection->upcomingPlaylist();
            action<KToggleAction>("saveUpcomingTracks")->setChecked(true);
            s >> *p;
            playlist = p;
            */
            break;
        }
        case Folder:
        {
            FolderPlaylist *p = new FolderPlaylist(collection);
            s >> *p;
            playlist = p;
            break;
        }
        default:
            Playlist *p = new Playlist(collection, true);
            s >> *p;

            // We may have already read this playlist from the folder
            // scanner, if an .m3u playlist
            if(collection->containsPlaylistFile(p->fileName())) {
                delete p;
                p = nullptr;
            }

            playlist = p;
            break;
        } // switch

        qint32 sortColumn;
        s >> sortColumn;
        if(playlist)
            playlist->sortByColumn(sortColumn);
    }
}
128

129 130
void Cache::loadPlaylists(PlaylistCollection *collection) // static
{
131
    const QString playlistsFile = playlistsCacheFileName();
132 133
    QFile f(playlistsFile);

Laurent Montel's avatar
Laurent Montel committed
134
    if(!f.open(QIODevice::ReadOnly))
135 136 137 138
        return;

    QDataStream fs(&f);

Laurent Montel's avatar
Laurent Montel committed
139
    qint32 version;
140
    fs >> version;
141 142 143 144 145 146

    if(version != 3 || fs.status() != QDataStream::Ok) {
        // Either the file is corrupt or is from a truly ancient version
        // of JuK.
        qCWarning(JUK_LOG) << "Found the playlist cache but it was clearly corrupt.";
        return;
147 148
    }

149 150
    // Our checksum is only for the values after the version and checksum so
    // we want to get a byte array with just the checksummed data.
151

152 153 154 155 156 157 158 159 160 161 162 163
    QByteArray data;
    quint16 checksum;
    fs >> checksum >> data;

    if(fs.status() != QDataStream::Ok || checksum != qChecksum(data.data(), data.size()))
        return;

    QDataStream s(&data, QIODevice::ReadOnly);
    s.setVersion(QDataStream::Qt_4_3);

    try { // Loading failures are indicated by an exception
        parsePlaylistStream(s, collection);
164
    }
165 166 167 168 169
    catch(BICStreamException &) {
        qCCritical(JUK_LOG) << "Exception loading playlists - binary incompatible stream.";
        // TODO Restructure the Playlist data model and PlaylistCollection data model
        // to be separate from the view/controllers.
        return;
170 171 172 173 174
    }
}

void Cache::savePlaylists(const PlaylistList &playlists)
{
175
    QString playlistsFile = playlistsCacheFileName();
Michael Pyne's avatar
Michael Pyne committed
176
    QSaveFile f(playlistsFile);
177

178
    if(!f.open(QIODevice::WriteOnly)) {
Michael Pyne's avatar
Michael Pyne committed
179
        qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString();
180
        return;
181
    }
182 183

    QByteArray data;
184
    QDataStream s(&data, QIODevice::WriteOnly);
185
    s.setVersion(QDataStream::Qt_4_3);
186

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
    for(const auto &it : playlists) {
        if(!(it)) {
            continue;
        }
        // TODO back serialization type into Playlist itself
        if(dynamic_cast<HistoryPlaylist *>(it)) {
            s << qint32(History)
                << *static_cast<HistoryPlaylist *>(it);
        }
        else if(dynamic_cast<SearchPlaylist *>(it)) {
            s << qint32(Search)
                << *static_cast<SearchPlaylist *>(it);
        }
        else if(dynamic_cast<UpcomingPlaylist *>(it)) {
            if(!action<KToggleAction>("saveUpcomingTracks")->isChecked())
                continue;
            s << qint32(Upcoming)
                << *static_cast<UpcomingPlaylist *>(it);
        }
        else if(dynamic_cast<FolderPlaylist *>(it)) {
            s << qint32(Folder)
                << *static_cast<FolderPlaylist *>(it);
        }
        else {
            s << qint32(Normal)
                << *(it);
213
        }
214
        s << qint32(it->sortColumn());
215 216 217
    }

    QDataStream fs(&f);
218
    fs << qint32(playlistListCacheVersion);
219 220 221 222
    fs << qChecksum(data.data(), data.size());

    fs << data;

Michael Pyne's avatar
Michael Pyne committed
223
    if(!f.commit())
Michael Pyne's avatar
Michael Pyne committed
224
        qCCritical(JUK_LOG) << "Error saving collection:" << f.errorString();
225 226
}

227 228 229 230 231 232 233 234 235
void Cache::ensureAppDataStorageExists() // static
{
    QString dirPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
    QDir appDataDir(dirPath);

    if(!appDataDir.exists() && !appDataDir.mkpath(dirPath))
        qCCritical(JUK_LOG) << "Unable to create appdata storage in" << dirPath;
}

236 237
bool Cache::cacheFileExists() // static
{
238 239 240 241 242 243 244 245 246 247 248 249 250
    return QFile::exists(fileHandleCacheFileName());
}

// Despite the 'Cache' class name, these data files are not regenerable and so
// should not be stored in cache directory.
QString Cache::fileHandleCacheFileName() // static
{
    return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/cache";
}

QString Cache::playlistsCacheFileName() // static
{
    return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/playlists";
251 252
}

253
////////////////////////////////////////////////////////////////////////////////
254
// private methods
255 256
////////////////////////////////////////////////////////////////////////////////

257
Cache::Cache()
258 259 260 261
{

}

262
bool Cache::prepareToLoadCachedItems()
263
{
264
    m_loadFile.setFileName(fileHandleCacheFileName());
265 266
    if(!m_loadFile.open(QIODevice::ReadOnly))
        return false;
267

268
    m_loadDataStream.setDevice(&m_loadFile);
269

270
    int dataStreamVersion = CacheDataStream::Qt_3_3;
271

Laurent Montel's avatar
Laurent Montel committed
272
    qint32 version;
273
    m_loadDataStream >> version;
274

275
    switch(version) {
276 277
    case 2:
        dataStreamVersion = CacheDataStream::Qt_4_3;
278
#if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0)
279
        Q_FALLTHROUGH();
280
#endif
281 282 283 284

        // Other than that we're compatible with cache v1, so fallthrough
        // to setCacheVersion

285
    case 1: {
286 287
        m_loadDataStream.setCacheVersion(1);
        m_loadDataStream.setVersion(dataStreamVersion);
288

Laurent Montel's avatar
Laurent Montel committed
289
        qint32 checksum;
290 291
        m_loadDataStream >> checksum
          >> m_loadFileBuffer.buffer();
292

293 294
        m_loadFileBuffer.open(QIODevice::ReadOnly);
        m_loadDataStream.setDevice(&m_loadFileBuffer);
295

296 297 298 299
        qint32 checksumExpected = qChecksum(
                m_loadFileBuffer.data(), m_loadFileBuffer.size());
        if(m_loadDataStream.status() != CacheDataStream::Ok ||
                checksum != checksumExpected)
300
        {
Michael Pyne's avatar
Michael Pyne committed
301
            qCCritical(JUK_LOG) << "Music cache checksum expected to get" << checksumExpected <<
302
                        "actually was" << checksum;
303 304
            KMessageBox::sorry(0, i18n("The music data cache has been corrupted. JuK "
                                       "needs to rescan it now. This may take some time."));
305
            return false;
306
        }
307

308
        break;
309 310
    }
    default: {
311 312
        m_loadDataStream.device()->reset();
        m_loadDataStream.setCacheVersion(0);
313 314 315

        // This cache is so old that this is just a wild guess here that 3.3
        // is compatible.
316
        m_loadDataStream.setVersion(CacheDataStream::Qt_3_3);
317
        break;
318 319 320
    }
    }

321 322 323 324 325 326
    return true;
}

FileHandle Cache::loadNextCachedItem()
{
    if(!m_loadFile.isOpen() || !m_loadDataStream.device()) {
Michael Pyne's avatar
Michael Pyne committed
327
        qCWarning(JUK_LOG) << "Already completed reading cache file.";
328
        return FileHandle();
329 330 331
    }

    if(m_loadDataStream.status() == QDataStream::ReadCorruptData) {
Michael Pyne's avatar
Michael Pyne committed
332
        qCCritical(JUK_LOG) << "Attempted to read file handle from corrupt cache file.";
333
        return FileHandle();
334
    }
335

336
    if(!m_loadDataStream.atEnd()) {
337
        QString fileName;
338
        m_loadDataStream >> fileName;
339
        fileName.squeeze();
340

341 342 343 344 345 346
        return FileHandle(fileName, m_loadDataStream);
    }
    else {
        m_loadDataStream.setDevice(0);
        m_loadFile.close();

347
        return FileHandle();
348 349
    }
}
350 351

// vim: set et sw=4 tw=0 sta: