PageRenderTime 635ms CodeModel.GetById 91ms app.highlight 476ms RepoModel.GetById 49ms app.codeStats 1ms

/src/libtomahawk/EchonestCatalogSynchronizer.cpp

http://github.com/tomahawk-player/tomahawk
C++ | 376 lines | 264 code | 62 blank | 50 comment | 25 complexity | ca982068e527dabca5cecf4a0d2e3f3f MD5 | raw file
  1/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
  2 *
  3 *   Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
  4 *   Copyright 2015, Christian Muehlhaeuser <muesli@tomahawk-player.org>
  5 *
  6 *   Tomahawk is free software: you can redistribute it and/or modify
  7 *   it under the terms of the GNU General Public License as published by
  8 *   the Free Software Foundation, either version 3 of the License, or
  9 *   (at your option) any later version.
 10 *
 11 *   Tomahawk is distributed in the hope that it will be useful,
 12 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 13 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14 *   GNU General Public License for more details.
 15 *
 16 *   You should have received a copy of the GNU General Public License
 17 *   along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 18 */
 19
 20#include "EchonestCatalogSynchronizer.h"
 21
 22#include "collection/Collection.h"
 23#include "database/Database.h"
 24#include "database/DatabaseImpl.h"
 25#include "database/DatabaseCommand_GenericSelect.h"
 26#include "database/DatabaseCommand_SetCollectionAttributes.h"
 27#include "database/DatabaseCommand_LoadFiles.h"
 28#include "database/DatabaseCommand_SetTrackAttributes.h"
 29#include "utils/Logger.h"
 30
 31#include "PlaylistEntry.h"
 32#include "Query.h"
 33#include "SourceList.h"
 34#include "TomahawkSettings.h"
 35#include "Track.h"
 36
 37#include <echonest5/CatalogUpdateEntry.h>
 38#include <echonest5/Config.h>
 39
 40using namespace Tomahawk;
 41
 42EchonestCatalogSynchronizer* EchonestCatalogSynchronizer::s_instance = 0;
 43
 44EchonestCatalogSynchronizer::EchonestCatalogSynchronizer( QObject *parent )
 45    : QObject( parent )
 46{
 47    m_syncing = TomahawkSettings::instance()->enableEchonestCatalogs();
 48
 49    qRegisterMetaType<QList<QStringList> >("QList<QStringList>");
 50
 51    connect( TomahawkSettings::instance(), SIGNAL( changed() ), this, SLOT( checkSettingsChanged() ) );
 52    connect( SourceList::instance()->getLocal()->dbCollection().data(), SIGNAL( tracksAdded( QList<unsigned int> ) ), this, SLOT( tracksAdded( QList<unsigned int> ) ), Qt::QueuedConnection );
 53    connect( SourceList::instance()->getLocal()->dbCollection().data(), SIGNAL( tracksRemoved( QList<unsigned int> ) ), this, SLOT( tracksRemoved( QList<unsigned int> ) ), Qt::QueuedConnection );
 54
 55    const QByteArray artist = TomahawkSettings::instance()->value( "collection/artistCatalog" ).toByteArray();
 56    const QByteArray song = TomahawkSettings::instance()->value( "collection/songCatalog" ).toByteArray();
 57
 58    if ( !artist.isEmpty() )
 59        m_artistCatalog.setId( artist );
 60    if ( !song.isEmpty() )
 61        m_songCatalog.setId( song );
 62
 63    // Sanity check
 64    if ( !song.isEmpty() && !m_syncing )
 65    {
 66        // Not syncing but have a catalog id... lets fix this
 67        QNetworkReply* r = m_songCatalog.deleteCatalog();
 68        connect( r, SIGNAL( finished() ), this, SLOT( catalogDeleted() ) );
 69        r->setProperty( "type", "song" );
 70    }
 71    if ( !artist.isEmpty() && !m_syncing )
 72    {
 73        QNetworkReply* r = m_artistCatalog.deleteCatalog();
 74        connect( r, SIGNAL( finished() ), this, SLOT( catalogDeleted() ) );
 75        r->setProperty( "type", "artist" );
 76    }
 77}
 78
 79void
 80EchonestCatalogSynchronizer::checkSettingsChanged()
 81{
 82    if ( TomahawkSettings::instance()->enableEchonestCatalogs() && !m_syncing )
 83    {
 84        // enable, and upload whole db
 85        m_syncing = true;
 86
 87        tDebug() << "Echonest Catalog sync pref changed, uploading!!";
 88        uploadDb();
 89    } else if ( !TomahawkSettings::instance()->enableEchonestCatalogs() && m_syncing )
 90    {
 91
 92        tDebug() << "Found echonest change, doing catalog deletes!";
 93        // delete all track nums and catalog ids from our peers
 94        {
 95            DatabaseCommand_SetTrackAttributes* cmd = new DatabaseCommand_SetTrackAttributes( DatabaseCommand_SetTrackAttributes::EchonestCatalogId );
 96            Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
 97        }
 98        {
 99            DatabaseCommand_SetCollectionAttributes* cmd = new DatabaseCommand_SetCollectionAttributes( DatabaseCommand_SetCollectionAttributes::EchonestSongCatalog, true );
100            Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
101        }
102
103        if ( !m_songCatalog.id().isEmpty() )
104        {
105            QNetworkReply* r = m_songCatalog.deleteCatalog();
106            connect( r, SIGNAL( finished() ), this, SLOT( catalogDeleted() ) );
107            r->setProperty( "type", "song" );
108        }
109        if ( !m_artistCatalog.id().isEmpty() )
110        {
111            QNetworkReply* r = m_artistCatalog.deleteCatalog();
112            connect( r, SIGNAL( finished() ), this, SLOT( catalogDeleted() ) );
113            r->setProperty( "type", "artist" );
114        }
115        m_syncing = false;
116    }
117}
118
119void
120EchonestCatalogSynchronizer::catalogDeleted()
121{
122    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
123    Q_ASSERT( r );
124    r->deleteLater();
125
126    QString toDel = QString( "collection/%1Catalog" ).arg( r->property( "type" ).toString() );
127
128    try
129    {
130        // HACK libechonest bug, should be a static method but it's not. Doesn't actually use any instance vars though
131        m_songCatalog.parseDelete( r );
132        // If we didn't throw, no errors, so clear our config
133        TomahawkSettings::instance()->setValue( toDel, QString() );
134    } catch ( const Echonest::ParseError& e )
135    {
136        tLog() << "Error in libechonest parsing catalog delete:" << e.what();
137    }
138}
139
140
141void
142EchonestCatalogSynchronizer::uploadDb()
143{
144    // create two catalogs: uuid_song, and uuid_artist.
145    QNetworkReply* r =  Echonest::Catalog::create( QString( "%1_song" ).arg( Database::instance()->impl()->dbid() ), Echonest::CatalogTypes::Song );
146    connect( r, SIGNAL( finished() ), this, SLOT( songCreateFinished() ) );
147
148//     r =  Echonest::Catalog::create( QString( "%1_artist" ).arg( Database::instance()->dbid() ), Echonest::CatalogTypes::Artist );
149//     connect( r, SIGNAL( finished() ), this, SLOT( artistCreateFinished() ) );
150}
151
152
153void
154EchonestCatalogSynchronizer::songCreateFinished()
155{
156    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
157    Q_ASSERT( r );
158    r->deleteLater();
159
160    tDebug() << "Finished creating song catalog, updating data now!!";
161    try
162    {
163        m_songCatalog = Echonest::Catalog::parseCreate( r );
164        TomahawkSettings::instance()->setValue( "collection/songCatalog", m_songCatalog.id() );
165        Tomahawk::dbcmd_ptr cmd( new DatabaseCommand_SetCollectionAttributes( DatabaseCommand_SetCollectionAttributes::EchonestSongCatalog,
166                                                                                            m_songCatalog.id() ) );
167        Database::instance()->enqueue( cmd );
168    } catch ( const Echonest::ParseError& e )
169    {
170        tLog() << "Echonest threw an exception parsing song catalog create:" << e.what();
171        return;
172    }
173
174    QString sql( "SELECT file.id, track.name, artist.name, album.name "
175                 "FROM file, artist, track, file_join "
176                 "LEFT OUTER JOIN album "
177                 "ON file_join.album = album.id "
178                 "WHERE file.id = file_join.file "
179                 "AND file_join.artist = artist.id "
180                 "AND file_join.track = track.id "
181                 "AND file.source IS NULL");
182    DatabaseCommand_GenericSelect* cmd = new DatabaseCommand_GenericSelect( sql, DatabaseCommand_GenericSelect::Track, true );
183    connect( cmd, SIGNAL( rawData( QList< QStringList > ) ), this, SLOT( rawTracksAdd( QList< QStringList > ) ) );
184    Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
185}
186
187
188void
189EchonestCatalogSynchronizer::artistCreateFinished()
190{
191    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
192    Q_ASSERT( r );
193    r->deleteLater();
194
195    // We don't support artist catalogs at the moment
196    return;
197    /*
198    try
199    {
200        m_artistCatalog = Echonest::Catalog::parseCreate( r );
201        TomahawkSettings::instance()->setValue( "collection/artistCatalog", m_artistCatalog.id() );
202
203//        Tomahawk::dbcmd_ptr cmd( new DatabaseCommand_SetCollectionAttributes( SourceList::instance()->getLocal(),
204//                                                                                            DatabaseCommand_SetCollectionAttributes::EchonestSongCatalog,
205//                                                                                            m_songCatalog.id() ) );
206//        Database::instance()->enqueue( cmd );
207    } catch ( const Echonest::ParseError& e )
208    {
209        tLog() << "Echonest threw an exception parsing artist catalog create:" << e.what();
210        return;
211    }*/
212}
213
214void
215EchonestCatalogSynchronizer::rawTracksAdd( const QList< QStringList >& tracks )
216{
217    tDebug() << "Got raw tracks, num:" << tracks.size();
218
219//     int limit = ( tracks.size() < 1000 ) ? tracks.size() : 1000;
220
221    int cur = 0;
222    while ( cur < tracks.size() )
223    {
224        int prev = cur;
225        cur = ( cur + 2000 > tracks.size() ) ? tracks.size() : cur + 2000;
226
227        tDebug() << "Enqueueing a batch of tracks to upload to echonest catalog:" << cur - prev;
228        Echonest::CatalogUpdateEntries entries;
229        for ( int i = prev; i < cur; i++ )
230        {
231            if ( tracks[i][1].isEmpty() || tracks[i][2].isEmpty() )
232                continue;
233            entries.append( entryFromTrack( tracks[i], Echonest::CatalogTypes::Update ) );
234        }
235        tDebug() << "Done queuing:" << entries.size() << "tracks";
236        m_queuedUpdates.enqueue( entries );
237    }
238
239    doUploadJob();
240
241}
242
243void
244EchonestCatalogSynchronizer::doUploadJob()
245{
246    if ( m_queuedUpdates.isEmpty() )
247        return;
248
249    Echonest::CatalogUpdateEntries entries = m_queuedUpdates.dequeue();
250    tDebug() << "Updating number of entries:" << entries.count();
251
252    QNetworkReply* updateJob = m_songCatalog.update( entries );
253    connect( updateJob, SIGNAL( finished() ), this, SLOT( songUpdateFinished() ) );
254}
255
256
257Echonest::CatalogUpdateEntry
258EchonestCatalogSynchronizer::entryFromTrack( const QStringList& track, Echonest::CatalogTypes::Action action ) const
259{
260    //qDebug() << "UPLOADING:" << track[0] << track[1] << track[2];
261    Echonest::CatalogUpdateEntry entry;
262    entry.setAction( action );
263    entry.setItemId(track[ 0 ].toLatin1() ); // track dbid
264    entry.setSongName( escape( track[ 1 ] ) );
265    entry.setArtistName( escape( track[ 2 ] ) );
266    entry.setRelease( escape( track[ 3 ] ) );
267
268    return entry;
269}
270
271
272void
273EchonestCatalogSynchronizer::songUpdateFinished()
274{
275    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
276    Q_ASSERT( r );
277    r->deleteLater();
278
279    try
280    {
281        QByteArray ticket = m_songCatalog.parseTicket( r );
282        QNetworkReply* tJob = m_songCatalog.status( ticket );
283        connect( tJob, SIGNAL( finished() ), this, SLOT( checkTicket() ) );
284    } catch ( const Echonest::ParseError& e )
285    {
286        tLog() << "Echonest threw an exception parsing catalog update finished:" << e.what();
287    }
288
289    doUploadJob();
290}
291
292void
293EchonestCatalogSynchronizer::checkTicket()
294{
295    QNetworkReply* r = qobject_cast< QNetworkReply* >( sender() );
296    Q_ASSERT( r );
297    r->deleteLater();
298
299    try
300    {
301        Echonest::CatalogStatus status = m_songCatalog.parseStatus( r );
302
303        tLog() << "Catalog status update:" << status.status << status.details << status.items;
304    } catch ( const Echonest::ParseError& e )
305    {
306        tLog() << "Echonest threw an exception parsing catalog create:" << e.what();
307        return;
308    }
309}
310
311void
312EchonestCatalogSynchronizer::tracksAdded( const QList< unsigned int >& tracks )
313{
314    if ( !m_syncing || m_songCatalog.id().isEmpty() || tracks.isEmpty() )
315        return;
316
317    qDebug() << Q_FUNC_INFO << "Got tracks added from db, fetching metadata" << tracks;
318    // Get the result_ptrs from the tracks
319    DatabaseCommand_LoadFiles* cmd = new DatabaseCommand_LoadFiles( tracks );
320    connect( cmd, SIGNAL( results( QList<Tomahawk::result_ptr> ) ), this, SLOT( loadedResults( QList<Tomahawk::result_ptr> ) ) );
321    Database::instance()->enqueue( Tomahawk::dbcmd_ptr( cmd ) );
322}
323
324
325void
326EchonestCatalogSynchronizer::loadedResults( const QList<result_ptr>& results )
327{
328    QList< QStringList > rawTracks;
329    qDebug() << Q_FUNC_INFO << "Got track metadata..." << results.size();
330
331    foreach( const result_ptr& result, results )
332    {
333        if ( result.isNull() )
334            continue;
335
336        qDebug() << "Metadata for item:" << result->fileId();
337
338        rawTracks << ( QStringList() << QString::number( result->fileId() ) << result->track()->track() << result->track()->artist() << result->track()->album() );
339    }
340    rawTracksAdd( rawTracks );
341}
342
343void
344EchonestCatalogSynchronizer::tracksRemoved( const QList< unsigned int >& trackIds )
345{
346
347    if ( !m_syncing || m_songCatalog.id().isEmpty() || trackIds.isEmpty() )
348        return;
349
350
351    Echonest::CatalogUpdateEntries entries;
352    entries.reserve( trackIds.size() );
353
354    foreach ( unsigned int id, trackIds )
355    {
356        Echonest::CatalogUpdateEntry e( Echonest::CatalogTypes::Delete );
357        e.setItemId( QString::number( id ).toLatin1() );
358        entries.append( e );
359    }
360
361    QNetworkReply* reply = m_songCatalog.update( entries );
362    connect( reply, SIGNAL( finished() ), this, SLOT( songUpdateFinished() ) );
363}
364
365QByteArray
366EchonestCatalogSynchronizer::escape( const QString &in ) const
367{
368    // TODO echonest chokes on some chars in the output. But if we percent-encode those chars it works
369    // We can't percent-encode the whole string, because then any UTF-8 chars that have been url-encoded, fail.
370    // God this sucks. It's going to break...
371    QString clean = in;
372    clean.replace( "&", "%25" );
373    clean.replace( ";", "%3B" );
374    return clean.toUtf8();
375    //return QUrl::toPercentEncoding( in. );
376}