Media.cpp 17.1 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*****************************************************************************
 * Media Library
 *****************************************************************************
 * Copyright (C) 2015 Hugo Beauzée-Luyssen, Videolabs
 *
 * Authors: Hugo Beauzée-Luyssen<hugo@beauzee.fr>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

23
#include <algorithm>
24
#include <cassert>
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
25 26
#include <cstdlib>
#include <cstring>
27
#include <ctime>
28

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
29
#include "Album.h"
30
#include "AlbumTrack.h"
31
#include "Artist.h"
32
#include "AudioTrack.h"
33
#include "Media.h"
34
#include "File.h"
35
#include "Folder.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
36
#include "Label.h"
37
#include "logging/Logger.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
38
#include "Movie.h"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
39
#include "ShowEpisode.h"
40
#include "database/SqliteTools.h"
41
#include "VideoTrack.h"
42
#include "filesystem/IFile.h"
43 44
#include "filesystem/IDirectory.h"
#include "filesystem/IDevice.h"
45
#include "utils/Filename.h"
46

47
const std::string policy::MediaTable::Name = "Media";
48
const std::string policy::MediaTable::PrimaryKeyColumn = "id_media";
49
unsigned int Media::* const policy::MediaTable::PrimaryKey = &Media::m_id;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
50

51 52
Media::Media( MediaLibraryPtr ml, sqlite::Row& row )
    : m_ml( ml )
53
    , m_changed( false )
54
{
55 56
    row >> m_id
        >> m_type
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
57
        >> m_subType
58 59
        >> m_duration
        >> m_playCount
60
        >> m_lastPlayedDate
61 62
        >> m_progress
        >> m_rating
63
        >> m_insertionDate
64
        >> m_releaseDate
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
65
        >> m_thumbnail
66
        >> m_title
67
        >> m_filename
68 69
        >> m_isFavorite
        >> m_isPresent;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
70 71
}

72 73 74
Media::Media( MediaLibraryPtr ml, const std::string& title, Type type )
    : m_ml( ml )
    , m_id( 0 )
75
    , m_type( type )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
76
    , m_subType( SubType::Unknown )
77
    , m_duration( -1 )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
78
    , m_playCount( 0 )
79
    , m_lastPlayedDate( 0 )
80 81
    , m_progress( .0f )
    , m_rating( -1 )
82
    , m_insertionDate( time( nullptr ) )
83
    , m_releaseDate( 0 )
84
    , m_title( title )
85 86
    // When creating a Media, meta aren't parsed, and therefor, is the filename
    , m_filename( title )
87
    , m_isFavorite( false )
88
    , m_changed( false )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
89
{
90 91
}

92
std::shared_ptr<Media> Media::create( MediaLibraryPtr ml, Type type, const fs::IFile& file )
93
{
94
    auto self = std::make_shared<Media>( ml, file.name(), type );
95
    static const std::string req = "INSERT INTO " + policy::MediaTable::Name +
96
            "(type, insertion_date, title, filename) VALUES(?, ?, ?, ?)";
97

98
    if ( insert( ml, self, req, type, self->m_insertionDate, self->m_title, self->m_filename ) == false )
99
        return nullptr;
100
    return self;
101 102
}

103
AlbumTrackPtr Media::albumTrack() const
104
{
105 106
    if ( m_subType != SubType::AlbumTrack )
        return nullptr;
107 108
    auto lock = m_albumTrack.lock();

109
    if ( m_albumTrack.isCached() == false )
110
        m_albumTrack = AlbumTrack::fromMedia( m_ml, m_id );
111
    return m_albumTrack.get();
112 113
}

114
void Media::setAlbumTrack( AlbumTrackPtr albumTrack )
115
{
116
    auto lock = m_albumTrack.lock();
117
    m_albumTrack = albumTrack;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
118 119
    m_subType = SubType::AlbumTrack;
    m_changed = true;
120 121
}

122
int64_t Media::duration() const
123 124 125 126
{
    return m_duration;
}

127
void Media::setDuration( int64_t duration )
128
{
129 130
    if ( m_duration == duration )
        return;
131
    m_duration = duration;
132
    m_changed = true;
133 134
}

135
ShowEpisodePtr Media::showEpisode() const
136
{
137 138
    if ( m_subType != SubType::ShowEpisode )
        return nullptr;
139

140 141
    auto lock = m_showEpisode.lock();
    if ( m_showEpisode.isCached() == false )
142
        m_showEpisode = ShowEpisode::fromMedia( m_ml, m_id );
143
    return m_showEpisode.get();
144 145
}

146
void Media::setShowEpisode( ShowEpisodePtr episode )
147
{
148 149
    auto lock = m_showEpisode.lock();
    m_showEpisode = episode;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
150 151
    m_subType = SubType::ShowEpisode;
    m_changed = true;
152 153
}

154
std::vector<LabelPtr> Media::labels()
155
{
156
    static const std::string req = "SELECT l.* FROM " + policy::LabelTable::Name + " l "
157
            "INNER JOIN LabelFileRelation lfr ON lfr.label_id = l.id_label "
158
            "WHERE lfr.media_id = ?";
159
    return Label::fetchAll<ILabel>( m_ml, req, m_id );
160 161
}

162
int Media::playCount() const
163
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
164
    return m_playCount;
165 166
}

167
bool Media::increasePlayCount()
168
{
169 170 171 172 173
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET "
            "play_count = ?, last_played_date = ? WHERE id_media = ?";
    auto lastPlayedDate = time( nullptr );
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, m_playCount + 1, lastPlayedDate, m_id ) == false )
        return false;
174
    m_playCount++;
175 176
    m_lastPlayedDate = lastPlayedDate;
    return true;
177 178
}

179 180 181 182 183
float Media::progress() const
{
    return m_progress;
}

184
bool Media::setProgress( float progress )
185
{
186
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET progress = ? WHERE id_media = ?";
187
    if ( progress == m_progress || progress < 0 || progress > 1.0 )
188 189 190
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, progress, m_id ) == false )
        return false;
191
    m_progress = progress;
192
    return true;
193 194 195 196 197 198 199
}

int Media::rating() const
{
    return m_rating;
}

200
bool Media::setRating( int rating )
201
{
202
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET rating = ? WHERE id_media = ?";
203
    if ( m_rating == rating )
204 205 206
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, rating, m_id ) == false )
        return false;
207
    m_rating = rating;
208
    return true;
209 210
}

211 212 213 214 215
bool Media::isFavorite() const
{
    return m_isFavorite;
}

216
bool Media::setFavorite( bool favorite )
217
{
218
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET is_favorite = ? WHERE id_media = ?";
219
    if ( m_isFavorite == favorite )
220 221 222
        return true;
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, favorite, m_id ) == false )
        return false;
223
    m_isFavorite = favorite;
224
    return true;
225 226
}

227
const std::vector<FilePtr>& Media::files() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
228
{
229 230 231 232 233
    auto lock = m_files.lock();
    if ( m_files.isCached() == false )
    {
        static const std::string req = "SELECT * FROM " + policy::FileTable::Name
                + " WHERE media_id = ?";
234
        m_files = File::fetchAll<IFile>( m_ml, req, m_id );
235 236
    }
    return m_files;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
237 238
}

239
MoviePtr Media::movie() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
240
{
241 242 243
    if ( m_subType != SubType::Movie )
        return nullptr;

244 245
    auto lock = m_movie.lock();

246
    if ( m_movie.isCached() == false )
247
        m_movie = Movie::fromMedia( m_ml, m_id );
248
    return m_movie.get();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
249 250
}

251
void Media::setMovie(MoviePtr movie)
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
252
{
253
    auto lock = m_movie.lock();
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
254
    m_movie = movie;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
255 256
    m_subType = SubType::Movie;
    m_changed = true;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
257 258
}

259
bool Media::addVideoTrack(const std::string& codec, unsigned int width, unsigned int height, float fps)
260
{
261
    return VideoTrack::create( m_ml, codec, width, height, fps, m_id ) != nullptr;
262 263
}

264
std::vector<VideoTrackPtr> Media::videoTracks()
265
{
266 267
    static const std::string req = "SELECT * FROM " + policy::VideoTrackTable::Name +
            " WHERE media_id = ?";
268
    return VideoTrack::fetchAll<IVideoTrack>( m_ml, req, m_id );
269 270
}

271
bool Media::addAudioTrack( const std::string& codec, unsigned int bitrate,
272 273
                          unsigned int sampleRate, unsigned int nbChannels,
                          const std::string& language, const std::string& desc )
274
{
275
    return AudioTrack::create( m_ml, codec, bitrate, sampleRate, nbChannels, language, desc, m_id ) != nullptr;
276 277
}

278
std::vector<AudioTrackPtr> Media::audioTracks()
279
{
280 281
    static const std::string req = "SELECT * FROM " + policy::AudioTrackTable::Name +
            " WHERE media_id = ?";
282
    return AudioTrack::fetchAll<IAudioTrack>( m_ml, req, m_id );
283 284
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
285
const std::string &Media::thumbnail()
286
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
287
    return m_thumbnail;
288 289
}

290 291 292 293 294
unsigned int Media::insertionDate() const
{
    return m_insertionDate;
}

295 296 297 298 299 300 301 302 303 304 305 306 307
unsigned int Media::releaseDate() const
{
    return m_releaseDate;
}

void Media::setReleaseDate( unsigned int date )
{
    if ( m_releaseDate == date )
        return;
    m_releaseDate = date;
    m_changed = true;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
308
void Media::setThumbnail(const std::string& thumbnail )
309
{
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
310
    if ( m_thumbnail == thumbnail )
311
        return;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
312
    m_thumbnail = thumbnail;
313 314 315 316 317 318
    m_changed = true;
}

bool Media::save()
{
    static const std::string req = "UPDATE " + policy::MediaTable::Name + " SET "
319
            "type = ?, subtype = ?, duration = ?, progress = ?, release_date = ?,"
320
            "thumbnail = ?, title = ? WHERE id_media = ?";
321 322
    if ( m_changed == false )
        return true;
323
    if ( sqlite::Tools::executeUpdate( m_ml->getConn(), req, m_type, m_subType, m_duration,
324
                                       m_progress, m_releaseDate, m_thumbnail, m_title, m_id ) == false )
325 326 327 328
    {
        return false;
    }
    m_changed = false;
329 330 331
    return true;
}

332
std::shared_ptr<File> Media::addFile( const fs::IFile& fileFs, Folder& parentFolder, fs::IDirectory& parentFolderFs, IFile::Type type )
333
{
334
    auto file = File::create( m_ml, m_id, type, fileFs, parentFolder.id(), parentFolderFs.device()->isRemovable() );
335 336 337 338 339 340
    if ( file == nullptr )
        return nullptr;
    auto lock = m_files.lock();
    if ( m_files.isCached() )
        m_files.get().push_back( file );
    return file;
341 342
}

343
void Media::removeFile( File& file )
344
{
345 346 347
    file.destroy();
    auto lock = m_files.lock();
    if ( m_files.isCached() == false )
348
        return;
349 350 351
    m_files.get().erase( std::remove_if( begin( m_files.get() ), end( m_files.get() ), [&file]( const FilePtr& f ) {
        return f->id() == file.id();
    }));
352 353
}

354
std::vector<MediaPtr> Media::listAll( MediaLibraryPtr ml, IMedia::Type type, medialibrary::SortingCriteria sort, bool desc )
355
{
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390
    std::string req;
    if ( sort == medialibrary::SortingCriteria::LastModificationDate )
    {
        req = "SELECT m.* FROM " + policy::MediaTable::Name + " m INNER JOIN "
                + policy::FileTable::Name + " f ON m.id_media = f.media_id"
                " WHERE m.type = ?"
                " AND ( f.type = ? OR f.type = ? )"
                " ORDER BY f.last_modification_date";
        if ( desc == true )
            req += " DESC";
        return fetchAll<IMedia>( ml, req, type, File::Type::Entire, File::Type::Main );
    }
    req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE type = ? AND is_present = 1 ORDER BY ";
    switch ( sort )
    {
    case medialibrary::SortingCriteria::Alpha:
    case medialibrary::SortingCriteria::Default:
        req += "title";
        break;
    case medialibrary::SortingCriteria::Duration:
        req += "duration";
        break;
    case medialibrary::SortingCriteria::InsertionDate:
        req += "insertion_date";
        break;
    case medialibrary::SortingCriteria::ReleaseDate:
        req += "release_date";
        break;
    default:
        break;
    }
    if ( desc == true )
        req += " DESC";

    return fetchAll<IMedia>( ml, req, type );
391 392
}

393
unsigned int Media::id() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
394 395 396 397
{
    return m_id;
}

398
IMedia::Type Media::type()
399 400 401 402
{
    return m_type;
}

Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
403 404 405 406 407
IMedia::SubType Media::subType() const
{
    return m_subType;
}

408
void Media::setType( Type type )
409
{
410 411
    if ( m_type != type )
        return;
412
    m_type = type;
413
    m_changed = true;
414 415
}

416
const std::string &Media::title() const
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
417
{
418
    return m_title;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
419 420
}

421
void Media::setTitle( const std::string &title )
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
422
{
423 424
    if ( m_title == title )
        return;
425
    m_title = title;
426
    m_changed = true;
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
427 428
}

429
bool Media::createTable( DBConnection connection )
430
{
431 432
    std::string req = "CREATE TABLE IF NOT EXISTS " + policy::MediaTable::Name + "("
            "id_media INTEGER PRIMARY KEY AUTOINCREMENT,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
433
            "type INTEGER,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
434
            "subtype INTEGER,"
435
            "duration INTEGER DEFAULT -1,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
436
            "play_count UNSIGNED INTEGER,"
437
            "last_played_date UNSIGNED INTEGER,"
438 439
            "progress REAL,"
            "rating INTEGER DEFAULT -1,"
440
            "insertion_date UNSIGNED INTEGER,"
441
            "release_date UNSIGNED INTEGER,"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
442
            "thumbnail TEXT,"
443
            "title TEXT,"
444
            "filename TEXT,"
445
            "is_favorite BOOLEAN NOT NULL DEFAULT 0,"
446
            "is_present BOOLEAN NOT NULL DEFAULT 1"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
447
            ")";
448 449
    static const std::string indexReq = "CREATE INDEX IF NOT EXISTS index_last_played_date ON "
            + policy::MediaTable::Name + "(last_played_date DESC)";
450 451
    static const std::string vtableReq = "CREATE VIRTUAL TABLE IF NOT EXISTS "
                + policy::MediaTable::Name + "Fts USING FTS3("
452 453
                "title,"
                "labels"
454 455
            ")";
    return sqlite::Tools::executeRequest( connection, req ) &&
456
            sqlite::Tools::executeRequest( connection, indexReq ) &&
457
            sqlite::Tools::executeRequest( connection, vtableReq );
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
}

bool Media::createTriggers( DBConnection connection )
{
    static const std::string triggerReq = "CREATE TRIGGER IF NOT EXISTS has_files_present AFTER UPDATE OF "
            "is_present ON " + policy::FileTable::Name +
            " BEGIN "
            " UPDATE " + policy::MediaTable::Name + " SET is_present="
                "(SELECT COUNT(id_file) FROM " + policy::FileTable::Name + " WHERE media_id=new.media_id AND is_present=1) "
                "WHERE id_media=new.media_id;"
            " END;";
    static const std::string triggerReq2 = "CREATE TRIGGER IF NOT EXISTS cascade_file_deletion AFTER DELETE ON "
            + policy::FileTable::Name +
            " BEGIN "
            " DELETE FROM " + policy::MediaTable::Name + " WHERE "
                "(SELECT COUNT(id_file) FROM " + policy::FileTable::Name + " WHERE media_id=old.media_id) = 0"
                " AND id_media=old.media_id;"
            " END;";
476 477 478 479

    static const std::string vtableInsertTrigger = "CREATE TRIGGER IF NOT EXISTS insert_media_fts"
            " AFTER INSERT ON " + policy::MediaTable::Name +
            " BEGIN"
480
            " INSERT INTO " + policy::MediaTable::Name + "Fts(rowid,title,labels) VALUES(new.id_media, new.title, '');"
481 482
            " END";
    static const std::string vtableDeleteTrigger = "CREATE TRIGGER IF NOT EXISTS delete_media_fts"
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
483
            " BEFORE DELETE ON " + policy::MediaTable::Name +
484 485 486 487 488 489 490 491
            " BEGIN"
            " DELETE FROM " + policy::MediaTable::Name + "Fts WHERE rowid = old.id_media;"
            " END";
    static const std::string vtableUpdateTitleTrigger2 = "CREATE TRIGGER IF NOT EXISTS update_media_title_fts"
              " AFTER UPDATE OF title ON " + policy::MediaTable::Name +
              " BEGIN"
              " UPDATE " + policy::MediaTable::Name + "Fts SET title = new.title WHERE rowid = new.id_media;"
              " END";
492
    return sqlite::Tools::executeRequest( connection, triggerReq ) &&
493 494 495 496
            sqlite::Tools::executeRequest( connection, triggerReq2 ) &&
            sqlite::Tools::executeRequest( connection, vtableInsertTrigger ) &&
            sqlite::Tools::executeRequest( connection, vtableDeleteTrigger ) &&
            sqlite::Tools::executeRequest( connection, vtableUpdateTitleTrigger2 );
497
}
Hugo Beauzée-Luyssen's avatar
Hugo Beauzée-Luyssen committed
498

499
bool Media::addLabel( LabelPtr label )
500
{
501
    if ( m_id == 0 || label->id() == 0 )
502
    {
503
        LOG_ERROR( "Both file & label need to be inserted in database before being linked together" );
504 505 506
        return false;
    }
    const char* req = "INSERT INTO LabelFileRelation VALUES(?, ?)";
507
    if ( sqlite::Tools::insert( m_ml->getConn(), req, label->id(), m_id ) == 0 )
508 509 510
        return false;
    const std::string reqFts = "UPDATE " + policy::MediaTable::Name + "Fts "
        "SET labels = labels || ' ' || ? WHERE rowid = ?";
511
    return sqlite::Tools::executeUpdate( m_ml->getConn(), reqFts, label->name(), m_id );
512 513
}

514
bool Media::removeLabel( LabelPtr label )
515
{
516
    if ( m_id == 0 || label->id() == 0 )
517
    {
518
        LOG_ERROR( "Can't unlink a label/file not inserted in database" );
519 520
        return false;
    }
521
    const char* req = "DELETE FROM LabelFileRelation WHERE label_id = ? AND media_id = ?";
522
    if ( sqlite::Tools::executeDelete( m_ml->getConn(), req, label->id(), m_id ) == false )
523 524 525
        return false;
    const std::string reqFts = "UPDATE " + policy::MediaTable::Name + "Fts "
            "SET labels = TRIM(REPLACE(labels, ?, '')) WHERE rowid = ?";
526
    return sqlite::Tools::executeUpdate( m_ml->getConn(), reqFts, label->name(), m_id );
527
}
528 529


530
std::vector<MediaPtr> Media::search( MediaLibraryPtr ml, const std::string& title )
531 532 533
{
    static const std::string req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE"
            " id_media IN (SELECT rowid FROM " + policy::MediaTable::Name + "Fts"
534 535
            " WHERE " + policy::MediaTable::Name + "Fts MATCH ?)"
            "AND is_present = 1";
536
    return Media::fetchAll<IMedia>( ml, req, title + "*" );
537
}
538

539
std::vector<MediaPtr> Media::fetchHistory( MediaLibraryPtr ml )
540 541 542
{
    static const std::string req = "SELECT * FROM " + policy::MediaTable::Name + " WHERE last_played_date IS NOT NULL"
            " ORDER BY last_played_date DESC LIMIT 100";
543
    return fetchAll<IMedia>( ml, req );
544
}