1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-01-19 15:37:01 +01:00

Merge branch 'spotifyplaylists'

This commit is contained in:
Leo Franchi 2012-04-14 23:52:08 -04:00
commit 701ef5a69d
63 changed files with 2828 additions and 383 deletions

View File

@ -1,6 +1,6 @@
#!/usr/bin/python
# This file is part of Tomahawk.
<<<<<<< HEAD
# This file is part of Tomahawk.
# It was inspired in large part by the macdeploy script in Clementine.
#
# Clementine is free software: you can redistribute it and/or modify

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -136,6 +136,7 @@
<file>data/images/rdio.png</file>
<file>data/images/grooveshark.png</file>
<file>data/images/lastfm-icon.png</file>
<file>data/images/spotifycore-logo.png</file>
<file>data/images/playlist-header-tiled.png</file>
<file>data/images/share.png</file>
<file>data/sql/dbmigrate-27_to_28.sql</file>

View File

@ -75,6 +75,8 @@ SET( tomahawkSourcesGui ${tomahawkSourcesGui}
accounts/lastfm/lastfmplugin.cpp
accounts/spotify/SpotifyAccount.cpp
accounts/spotify/SpotifyAccountConfig.cpp
accounts/spotify/SpotifyPlaylistUpdater.cpp
tomahawktrayicon.cpp
audiocontrols.cpp
@ -101,6 +103,7 @@ SET( tomahawkUI ${tomahawkUI}
proxydialog.ui
accounts/lastfm/LastFmConfig.ui
accounts/spotify/SpotifyAccountConfig.ui
audiocontrols.ui
LoadXSPFDialog.ui

View File

@ -17,6 +17,7 @@
*/
#include "LoadXSPFDialog.h"
#include "tomahawksettings.h"
#include "ui_LoadXSPFDialog.h"
#include <QFileDialog>
@ -44,7 +45,11 @@ LoadXSPFDialog::~LoadXSPFDialog()
void
LoadXSPFDialog::getLocalFile()
{
QString url = QFileDialog::getOpenFileName( this, tr( "Load XSPF File" ), QDir::homePath(), tr( "XSPF Files (*.xspf)" ) );
const QString path = TomahawkSettings::instance()->importXspfPath();
QString url = QFileDialog::getOpenFileName( this, tr( "Load XSPF File" ), path, tr( "XSPF Files (*.xspf)" ) );
if ( !url.isEmpty() )
TomahawkSettings::instance()->setImportXspfPath( QFileInfo( url ).absoluteDir().absolutePath() );
m_ui->lineEdit->setText( url );
}

View File

@ -95,4 +95,3 @@ private:
}
#endif // LASTFMPLUGIN_H

View File

@ -19,15 +19,27 @@
#include "SpotifyAccount.h"
#include "playlist.h"
#include "utils/tomahawkutils.h"
#include "playlist/PlaylistUpdaterInterface.h"
#include "sourcelist.h"
#include "SpotifyAccountConfig.h"
#include "SpotifyPlaylistUpdater.h"
#include "resolvers/scriptresolver.h"
#include "utils/tomahawkutils.h"
#include "actioncollection.h"
#ifndef ENABLE_HEADLESS
#include "jobview/JobStatusView.h"
#include "jobview/JobStatusModel.h"
#include "jobview/ErrorStatusMessage.h"
#endif
#include <QPixmap>
#include <QAction>
using namespace Tomahawk;
using namespace Accounts;
static QPixmap* s_icon = 0;
Account*
@ -65,14 +77,330 @@ SpotifyAccountFactory::icon() const
SpotifyAccount::SpotifyAccount( const QString& accountId )
: ResolverAccount( accountId )
{
init();
}
SpotifyAccount::SpotifyAccount( const QString& accountId, const QString& path )
: ResolverAccount( accountId, path )
{
init();
}
SpotifyAccount::~SpotifyAccount()
{
clearUser();
}
void
SpotifyAccount::init()
{
qRegisterMetaType< Tomahawk::Accounts::SpotifyPlaylistInfo* >( "Tomahawk::Accounts::SpotifyPlaylist*" );
m_spotifyResolver = dynamic_cast< ScriptResolver* >( m_resolver.data() );
connect( m_spotifyResolver.data(), SIGNAL( customMessage( QString,QVariantMap ) ), this, SLOT( resolverMessage( QString, QVariantMap ) ) );
const bool hasMigrated = configuration().value( "hasMigrated" ).toBool();
if ( !hasMigrated )
{
qDebug() << "Getting credentials from spotify resolver to migrate to in-app config";
QVariantMap msg;
msg[ "_msgtype" ] = "getCredentials";
m_spotifyResolver.data()->sendMessage( msg );
}
QAction* action = new QAction( 0 );
action->setIcon( QIcon( RESPATH "images/spotify-logo.png" ) );
connect( action, SIGNAL( triggered( bool ) ), this, SLOT( syncActionTriggered( bool ) ) );
ActionCollection::instance()->addAction( ActionCollection::LocalPlaylists, action, this );
m_customActions.append( action );
}
void
SpotifyAccount::aboutToShow( QAction* action, const playlist_ptr& playlist )
{
if ( !m_customActions.contains( action ) )
return;
// If it's not being synced, allow the option to sync
SpotifyPlaylistUpdater* updater = qobject_cast< SpotifyPlaylistUpdater* >( playlist->updater() );
if ( !updater || !updater->sync() )
{
action->setText( tr( "Sync with Spotify" ) );
}
else
{
action->setText( tr( "Stop syncing with Spotify" ) );
}
}
void
SpotifyAccount::syncActionTriggered( bool checked )
{
Q_UNUSED( checked );
QAction* action = qobject_cast< QAction* >( sender() );
if ( !action || !m_customActions.contains( action ) )
return;
const playlist_ptr playlist = action->property( "payload" ).value< playlist_ptr >();
if ( playlist.isNull() )
{
qWarning() << "Got context menu spotify sync action triggered, but invalid playlist payload!";
Q_ASSERT( false );
return;
}
SpotifyPlaylistUpdater* updater = qobject_cast< SpotifyPlaylistUpdater* >( playlist->updater() );
if ( !updater )
{
QVariantMap msg;
msg[ "_msgtype" ] = "createPlaylist";
msg[ "sync" ] = true;
msg[ "title" ] = playlist->title();
QList< query_ptr > queries;
foreach ( const plentry_ptr& ple, playlist->entries() )
queries << ple->query();
QVariantList tracks = SpotifyPlaylistUpdater::queriesToVariant( queries );
msg[ "tracks" ] = tracks;
const QString qid = sendMessage( msg, this, "playlistCreated" );
m_waitingForCreateReply[ qid ] = playlist;
}
else
{
SpotifyPlaylistInfo* info = 0;
foreach ( SpotifyPlaylistInfo* ifo, m_allSpotifyPlaylists )
{
if ( ifo->plid == updater->spotifyId() )
{
info = ifo;
break;
}
}
Q_ASSERT( info );
if ( info )
info->sync = !updater->sync();
if ( m_configWidget.data() )
m_configWidget.data()->setPlaylists( m_allSpotifyPlaylists );
if ( !updater->sync() )
{
startPlaylistSync( info );
}
else
{
stopPlaylistSync( info, true );
}
}
}
void
SpotifyAccount::resolverMessage( const QString &msgType, const QVariantMap &msg )
{
if ( msgType == "credentials" )
{
QVariantHash creds = credentials();
creds[ "username" ] = msg.value( "username" );
creds[ "password" ] = msg.value( "password" );
creds[ "highQuality" ] = msg.value( "highQuality" );
setCredentials( creds );
qDebug() << "Set creds:" << creds.value( "username" ) << creds.value( "password" ) << msg.value( "username" ) << msg.value( "password" );
QVariantHash config = configuration();
config[ "hasMigrated" ] = true;
setConfiguration( config );
sync();
return;
}
const QString qid = msg.value( "qid" ).toString();
if ( m_qidToSlotMap.contains( qid ) )
{
QObject* receiver = m_qidToSlotMap[ qid ].first;
QString slot = m_qidToSlotMap[ qid ].second;
m_qidToSlotMap.remove( qid );
QMetaObject::invokeMethod( receiver, slot.toLatin1(), Q_ARG( QString, msgType ), Q_ARG( QVariantMap, msg ) );
}
else if ( msgType == "allPlaylists" )
{
const QVariantList playlists = msg.value( "playlists" ).toList();
qDeleteAll( m_allSpotifyPlaylists );
m_allSpotifyPlaylists.clear();
foreach ( const QVariant& playlist, playlists )
{
const QVariantMap plMap = playlist.toMap();
const QString name = plMap.value( "name" ).toString();
const QString plid = plMap.value( "id" ).toString();
const QString revid = plMap.value( "revid" ).toString();
const bool sync = plMap.value( "sync" ).toBool();
if ( name.isNull() || plid.isNull() || revid.isNull() )
{
qDebug() << "Did not get name and plid and revid for spotify playlist:" << name << plid << revid << plMap;
continue;
}
m_allSpotifyPlaylists << new SpotifyPlaylistInfo( name, plid, revid, sync );
}
if ( !m_configWidget.isNull() )
{
m_configWidget.data()->setPlaylists( m_allSpotifyPlaylists );
}
}
else if ( msgType == "tracksAdded" )
{
const QString plid = msg.value( "playlistid" ).toString();
// We should already be syncing this playlist if we get updates for it
// Q_ASSERT( m_updaters.contains( plid ) );
if ( !m_updaters.contains( plid ) )
return;
SpotifyPlaylistUpdater* updater = m_updaters[ plid ];
Q_ASSERT( updater->sync() );
const QString startPos = msg.value( "startPosition" ).toString();
const QVariantList tracksList = msg.value( "tracks" ).toList();
const QString newRev = msg.value( "revid" ).toString();
const QString oldRev = msg.value( "oldRev" ).toString();
updater->spotifyTracksAdded( tracksList, startPos, newRev, oldRev );
}
else if ( msgType == "tracksRemoved" )
{
const QString plid = msg.value( "playlistid" ).toString();
// We should already be syncing this playlist if we get updates for it
// Q_ASSERT( m_updaters.contains( plid ) );
if ( !m_updaters.contains( plid ) )
return;
SpotifyPlaylistUpdater* updater = m_updaters[ plid ];
// If we're not syncing with this, the resolver is quite misinformed.
// Q_ASSERT( updater && updater->sync() );
if ( !updater || !updater->sync() )
return;
const QVariantList tracksList = msg.value( "trackPositions" ).toList();
const QString newRev = msg.value( "revid" ).toString();
const QString oldRev = msg.value( "oldRev" ).toString();
updater->spotifyTracksRemoved( tracksList, newRev, oldRev );
}
else if ( msgType == "tracksMoved" )
{
const QString plid = msg.value( "playlistid" ).toString();
// We should already be syncing this playlist if we get updates for it
Q_ASSERT( m_updaters.contains( plid ) );
if ( !m_updaters.contains( plid ) )
return;
SpotifyPlaylistUpdater* updater = m_updaters[ plid ];
Q_ASSERT( updater->sync() );
const QString newStartPos = msg.value( "newStartPosition" ).toString();
const QVariantList tracksList = msg.value( "tracks" ).toList();
const QString newRev = msg.value( "revid" ).toString();
const QString oldRev = msg.value( "oldRev" ).toString();
updater->spotifyTracksMoved( tracksList, newStartPos, newRev, oldRev );
}
else if( msgType == "playlistRenamed" )
{
const QString plid = msg.value( "id" ).toString();
// We should already be syncing this playlist if we get updates for it
//Q_ASSERT( m_updaters.contains( plid ) );
qDebug() << Q_FUNC_INFO;
if ( !m_updaters.contains( plid ) )
return;
SpotifyPlaylistUpdater* updater = m_updaters[ plid ];
Q_ASSERT( updater->sync() );
qDebug() << "Playlist renamed fetched in tomahawk";
const QString title = msg.value( "name" ).toString();
const QString newRev = msg.value( "revid" ).toString();
const QString oldRev = msg.value( "oldRev" ).toString();
updater->spotifyPlaylistRenamed( title, newRev, oldRev );
}
else if( msgType == "spotifyError" )
{
const QString error = msg.value( "msg" ).toString();
if( error.isEmpty() )
return;
if( msg.value( "isDebugMsg" ).toBool() )
tDebug( LOGVERBOSE ) << "SpotifyResolverError: " << error;
else
JobStatusView::instance()->model()->addJob( new ErrorStatusMessage( QString( "Spotify: %1" ).arg( error ) ) );
}
else if( msgType == "userChanged" )
{
const QString rmsg = msg.value( "msg" ).toString();
clearUser();
if ( m_configWidget.data() )
m_configWidget.data()->setPlaylists( QList< SpotifyPlaylistInfo* >() );
qDebug() << "User changed message from spotify:" << rmsg;
}
else if ( msgType == "loginResponse" )
{
QVariantHash creds = credentials();
creds[ "username" ] = msg.value( "username" ).toString();
creds[ "password" ] = msg.value( "password" ).toString();
creds[ "highQuality" ] = msg.value( "highQuality" ).toString();
setCredentials( creds );
sync();
configurationWidget(); // ensure it's created so we can set the login button
if ( m_configWidget.data() )
{
const bool success = msg.value( "success" ).toBool();
const QString message = msg.value( "message" ).toString();
m_configWidget.data()->loginResponse( success, message );
}
}
}
void
SpotifyAccount::clearUser()
{
foreach( SpotifyPlaylistUpdater* updater, m_updaters.values() )
updater->remove();
m_updaters.clear();
qDeleteAll( m_allSpotifyPlaylists );
m_allSpotifyPlaylists.clear();
m_qidToSlotMap.clear();
m_waitingForCreateReply.clear();
foreach( QAction* action, m_customActions )
ActionCollection::instance()->removeAction( action );
}
@ -86,63 +414,273 @@ SpotifyAccount::icon() const
}
void
SpotifyAccount::addPlaylist( const QString &qid, const QString& title, QList< Tomahawk::query_ptr > tracks )
QWidget*
SpotifyAccount::configurationWidget()
{
/* Sync sync;
sync.id_ = qid;
int index = m_syncPlaylists.indexOf( sync );
if( !m_syncPlaylists.contains( sync ) )
if ( m_configWidget.isNull() )
{
qDebug() << Q_FUNC_INFO << "Adding playlist to sync" << qid;
playlist_ptr pl;
pl = Tomahawk::Playlist::create( SourceList::instance()->getLocal(),
uuid(),
title,
QString(),
QString(),
false,
tracks );
sync.playlist = pl;
sync.uuid = pl->guid();
m_syncPlaylists.append( sync );
m_configWidget = QWeakPointer< SpotifyAccountConfig >( new SpotifyAccountConfig( this ) );
connect( m_configWidget.data(), SIGNAL( login( QString,QString ) ), this, SLOT( login( QString,QString ) ) );
m_configWidget.data()->setPlaylists( m_allSpotifyPlaylists );
}
return static_cast< QWidget* >( m_configWidget.data() );
}
void
SpotifyAccount::saveConfig()
{
Q_ASSERT( !m_configWidget.isNull() );
if ( m_configWidget.isNull() )
return;
QVariantHash creds = credentials();
if ( creds.value( "username" ).toString() != m_configWidget.data()->username() ||
creds.value( "password" ).toString() != m_configWidget.data()->password() ||
creds.value( "highQuality" ).toBool() != m_configWidget.data()->highQuality() )
{
creds[ "username" ] = m_configWidget.data()->username();
creds[ "password" ] = m_configWidget.data()->password();
creds[ "highQuality" ] = m_configWidget.data()->highQuality();
setCredentials( creds );
}
QVariantHash config = configuration();
config[ "deleteOnUnsync" ] = m_configWidget.data()->deleteOnUnsync();
setConfiguration( config );
m_configWidget.data()->saveSettings();
foreach ( SpotifyPlaylistInfo* pl, m_allSpotifyPlaylists )
{
// qDebug() << "Checking changed state:" << pl->changed << pl->name << pl->sync;
if ( pl->changed )
{
pl->changed = false;
if ( pl->sync )
{
// Fetch full playlist contents, then begin the sync
startPlaylistSync( pl );
}
else
stopPlaylistSync( pl );
}
}
sync();
}
void
SpotifyAccount::login( const QString& username, const QString& password )
{
// Send the result to the resolver
QVariantMap msg;
msg[ "_msgtype" ] = "login";
msg[ "username" ] = username;
msg[ "password" ] = password;
msg[ "highQuality" ] = m_configWidget.data()->highQuality();
m_spotifyResolver.data()->sendMessage( msg );
}
void
SpotifyAccount::startPlaylistSync( SpotifyPlaylistInfo* playlist )
{
if ( !playlist )
return;
QVariantMap msg;
msg[ "_msgtype" ] = "getPlaylist";
msg[ "playlistid" ] = playlist->plid;
msg[ "sync" ] = playlist->sync;
sendMessage( msg, this, "startPlaylistSyncWithPlaylist" );
}
void
SpotifyAccount::startPlaylistSyncWithPlaylist( const QString& msgType, const QVariantMap& msg )
{
Q_UNUSED( msgType );
qDebug() << Q_FUNC_INFO << "Got full spotify playlist body, creating a tomahawk playlist and enabling sync!!";
const QString id = msg.value( "id" ).toString();
const QString name = msg.value( "name" ).toString();
const QString revid = msg.value( "revid" ).toString();
qDebug() << "Starting sync with pl:" << id << name;
QVariantList tracks = msg.value( "tracks" ).toList();
// create a list of query/plentries directly
QList< query_ptr > queries = SpotifyPlaylistUpdater::variantToQueries( tracks );
/**
* Begin syncing a playlist. Two options:
* 1) This is a playlist that has never been synced to tomahawk. Create a new one
* and attach a new SpotifyPlaylistUpdater to it
* 2) This was previously synced, and has since been unsynced. THe playlist is still around
* with an inactive SpotifyPlaylistUpdater, so just enable it and bring it up to date by merging current with new
* TODO: show a warning( "Do you want to overwrite with spotify's version?" )
*/
if ( m_updaters.contains( id ) )
{
Q_ASSERT( m_updaters[ id ]->sync() == false ); /// Should have been unchecked/off before
m_updaters[ id ]->setSync( true );
// m_updaters[ id ]->
// TODO
}
else
{
playlist_ptr plPtr = Tomahawk::Playlist::create( SourceList::instance()->getLocal(),
uuid(),
name,
QString(),
QString(),
false,
queries );
qDebug() << Q_FUNC_INFO << "Found playlist";
SpotifyPlaylistUpdater* updater = new SpotifyPlaylistUpdater( this, revid, id, plPtr );
updater->setSync( true );
m_updaters[ id ] = updater;
}
}
if ( index != -1 && !tracks.isEmpty())
{
qDebug() << Q_FUNC_INFO << "Got pl" << m_syncPlaylists[ index ].playlist->guid();
void
SpotifyAccount::playlistCreated( const QString& msgType, const QVariantMap& msg )
{
Q_UNUSED( msgType );
QList< query_ptr > currTracks;
foreach ( const plentry_ptr ple, m_syncPlaylists[ index ].playlist->entries() )
currTracks << ple->query();
qDebug() << Q_FUNC_INFO << "Got response from our createPlaylist command, now creating updater and attaching";
const bool success = msg.value( "success" ).toBool();
qDebug() << Q_FUNC_INFO << "tracks" << currTracks;
bool changed = false;
QList< query_ptr > mergedTracks = TomahawkUtils::mergePlaylistChanges( currTracks, tracks, changed );
if ( changed )
{
QList<Tomahawk::plentry_ptr> el = m_syncPlaylists[ index ].playlist->entriesFromQueries( mergedTracks, true );
m_syncPlaylists[ index ].playlist->createNewRevision( uuid(), m_syncPlaylists[ index ].playlist->currentrevision(), el );
}
}
if ( !success )
{
qWarning() << "Got FAILED return code from spotify resolver createPlaylist command, aborting sync";
return;
}
*/
const QString id = msg.value( "playlistid" ).toString();
const QString revid = msg.value( "playlistid" ).toString();
const QString qid = msg.value( "qid" ).toString();
if ( !m_waitingForCreateReply.contains( qid ) )
{
qWarning() << "Got a createPlaylist reply for a playlist/qid we were not waiting for :-/ " << qid << m_waitingForCreateReply;
return;
}
playlist_ptr playlist = m_waitingForCreateReply.take( qid );
SpotifyPlaylistUpdater* updater = new SpotifyPlaylistUpdater( this, revid, id, playlist );
updater->setSync( true );
m_updaters[ id ] = updater;
}
QString
SpotifyAccount::sendMessage( const QVariantMap &m, QObject* obj, const QString& slot )
{
QVariantMap msg = m;
QString qid;
if ( obj )
{
qid = QUuid::createUuid().toString().replace( "{", "" ).replace( "}", "" );
m_qidToSlotMap[ qid ] = qMakePair( obj, slot );
msg[ "qid" ] = qid;
}
m_spotifyResolver.data()->sendMessage( msg );
return qid;
}
void
SpotifyAccount::registerUpdaterForPlaylist( const QString& plId, SpotifyPlaylistUpdater* updater )
{
m_updaters[ plId ] = updater;
}
void
SpotifyAccount::unregisterUpdater( const QString& plid )
{
m_updaters.remove( plid );
}
void
SpotifyAccount::fetchFullPlaylist( SpotifyPlaylistInfo* playlist )
{
}
bool
operator==( Tomahawk::Accounts::SpotifyAccount::Sync one, Tomahawk::Accounts::SpotifyAccount::Sync two )
SpotifyAccount::deleteOnUnsync() const
{
if( one.id_ == two.id_ )
return true;
return false;
return configuration().value( "deleteOnUnsync", false ).toBool();
}
void
SpotifyAccount::stopPlaylistSync( SpotifyPlaylistInfo* playlist, bool forceDontDelete )
{
if ( !playlist )
return;
QVariantMap msg;
msg[ "_msgtype" ] = "removeFromSyncList";
msg[ "playlistid" ] = playlist->plid;
m_spotifyResolver.data()->sendMessage( msg );
if ( m_updaters.contains( playlist->plid ) )
{
SpotifyPlaylistUpdater* updater = m_updaters[ playlist->plid ];
updater->setSync( false );
if ( deleteOnUnsync() && !forceDontDelete )
{
playlist_ptr tomahawkPl = updater->playlist();
if ( !tomahawkPl.isNull() )
Playlist::remove( tomahawkPl );
updater->deleteLater();
}
updater->save();
}
}
void
SpotifyAccount::loadPlaylists()
{
// TODO cache this and only get changed?
QVariantMap msg;
msg[ "_msgtype" ] = "getAllPlaylists";
sendMessage( msg, this, "allPlaylistsLoaded" );
}
void
SpotifyAccount::setSyncForPlaylist( const QString& spotifyPlaylistId, bool sync )
{
foreach ( SpotifyPlaylistInfo* info, m_allSpotifyPlaylists )
{
if( info->plid == spotifyPlaylistId )
info->sync = sync;
}
if ( !m_configWidget.isNull() )
m_configWidget.data()->setPlaylists( m_allSpotifyPlaylists );
}

View File

@ -20,19 +20,35 @@
#ifndef SpotifyAccount_H
#define SpotifyAccount_H
#include "accounts/ResolverAccount.h"
#include "sourcelist.h"
#include "playlist.h"
#include "utils/tomahawkutils.h"
#include "sourcelist.h"
#include "accounts/ResolverAccount.h"
#include "utils/SmartPointerList.h"
class QAction;
class SpotifyPlaylistUpdater;
class QTimer;
class ScriptResolver;
namespace Tomahawk {
class ExternalResolverGui;
namespace Accounts {
class SpotifyAccountConfig;
// metadata for a playlist
struct SpotifyPlaylistInfo {
QString name, plid, revid;
bool sync, changed;
SpotifyPlaylistInfo( const QString& nname, const QString& pid, const QString& rrevid, bool ssync )
: name( nname ), plid( pid ), revid( rrevid ), sync( ssync ), changed( false ) {}
SpotifyPlaylistInfo() : sync( false ), changed( false ) {}
};
class SpotifyAccountFactory : public AccountFactory
{
@ -61,27 +77,65 @@ class SpotifyAccount : public ResolverAccount
public:
SpotifyAccount( const QString& accountId );
SpotifyAccount( const QString& accountId, const QString& path );
virtual ~SpotifyAccount() {}
virtual ~SpotifyAccount();
virtual QPixmap icon() const;
virtual QWidget* configurationWidget();
virtual void saveConfig();
virtual QWidget* aclWidget() { return 0; }
virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() { return Tomahawk::InfoSystem::InfoPluginPtr(); }
virtual SipPlugin* sipPlugin() { return 0; }
void addPlaylist( const QString &qid, const QString& title, QList< Tomahawk::query_ptr > tracks );
QString sendMessage( const QVariantMap& msg, QObject* receiver = 0, const QString& slot = QString() );
struct Sync {
QString id_;
QString uuid;
Tomahawk::playlist_ptr playlist;
};
void registerUpdaterForPlaylist( const QString& plId, SpotifyPlaylistUpdater* updater );
void unregisterUpdater( const QString& plid );
bool deleteOnUnsync() const;
public slots:
void aboutToShow( QAction* action, const Tomahawk::playlist_ptr& playlist );
void syncActionTriggered( bool );
private slots:
void resolverMessage( const QString& msgType, const QVariantMap& msg );
void login( const QString& username, const QString& password );
// SpotifyResolver message handlers, all take msgtype, msg as argument
// void <here>( const QString& msgType, const QVariantMap& msg );
void startPlaylistSyncWithPlaylist( const QString& msgType, const QVariantMap& msg );
void playlistCreated( const QString& msgType, const QVariantMap& msg );
private:
QList<Sync> m_syncPlaylists;
void init();
void loadPlaylists();
void clearUser();
void startPlaylistSync( SpotifyPlaylistInfo* playlist );
void stopPlaylistSync( SpotifyPlaylistInfo* playlist, bool forceDontDelete = false );
void fetchFullPlaylist( SpotifyPlaylistInfo* playlist );
void setSyncForPlaylist( const QString& spotifyPlaylistId, bool sync );
QWeakPointer<SpotifyAccountConfig> m_configWidget;
QWeakPointer<ScriptResolver> m_spotifyResolver;
QMap<QString, QPair<QObject*, QString> > m_qidToSlotMap;
// List of synced spotify playlists in config UI
QList< SpotifyPlaylistInfo* > m_allSpotifyPlaylists;
QHash< QString, SpotifyPlaylistUpdater* > m_updaters;
QHash< QString, playlist_ptr > m_waitingForCreateReply;
SmartPointerList< QAction > m_customActions;
friend class ::SpotifyPlaylistUpdater;
};
}
}
}
Q_DECLARE_METATYPE( Tomahawk::Accounts::SpotifyPlaylistInfo* );
#endif // SpotifyAccount_H

View File

@ -0,0 +1,165 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk 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 3 of the License, or
* (at your option) any later version.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SpotifyAccountConfig.h"
#include "SpotifyAccount.h"
#include <playlist/dynamic/widgets/LoadingSpinner.h>
#include "ui_SpotifyAccountConfig.h"
#include <QListWidget>
#include <QListWidgetItem>
#include <QShowEvent>
using namespace Tomahawk;
using namespace Accounts;
SpotifyAccountConfig::SpotifyAccountConfig( SpotifyAccount *account )
: QWidget( 0 )
, m_ui( new Ui::SpotifyConfig )
, m_account( account )
, m_playlistsLoading( 0 )
{
m_ui->setupUi( this );
connect( m_ui->loginButton, SIGNAL( clicked( bool ) ), this, SLOT( doLogin() ) );
connect( m_ui->usernameEdit, SIGNAL( textChanged( QString ) ), this, SLOT( resetLoginButton() ) );
connect( m_ui->passwordEdit, SIGNAL( textChanged( QString ) ), this, SLOT( resetLoginButton() ) );
loadFromConfig();
m_playlistsLoading = new LoadingSpinner( m_ui->playlistList );
}
void
SpotifyAccountConfig::showEvent( QShowEvent *event )
{
loadFromConfig();
}
void
SpotifyAccountConfig::loadFromConfig()
{
m_ui->usernameEdit->setText( m_account->credentials().value( "username" ).toString() );
m_ui->passwordEdit->setText( m_account->credentials().value( "password" ).toString() );
m_ui->streamingCheckbox->setChecked( m_account->credentials().value( "highQuality" ).toBool() );
m_ui->deleteOnUnsync->setChecked( m_account->deleteOnUnsync() );
}
void
SpotifyAccountConfig::saveSettings()
{
for( int i = 0; i < m_ui->playlistList->count(); i++ )
{
const QListWidgetItem* item = m_ui->playlistList->item( i );
SpotifyPlaylistInfo* pl = item->data( Qt::UserRole ).value< SpotifyPlaylistInfo* >();
const bool toSync = ( item->checkState() == Qt::Checked );
if ( pl->sync != toSync )
{
pl->changed = true;
pl->sync = toSync;
}
}
}
QString
SpotifyAccountConfig::username() const
{
return m_ui->usernameEdit->text().trimmed();
}
QString
SpotifyAccountConfig::password() const
{
return m_ui->passwordEdit->text().trimmed();
}
bool
SpotifyAccountConfig::highQuality() const
{
return m_ui->streamingCheckbox->isChecked();
}
bool
SpotifyAccountConfig::deleteOnUnsync() const
{
return m_ui->deleteOnUnsync->isChecked();
}
void
SpotifyAccountConfig::setPlaylists( const QList<SpotifyPlaylistInfo *>& playlists )
{
// User always has at least 1 playlist (starred tracks)
if ( !playlists.isEmpty() )
m_playlistsLoading->fadeOut();
m_ui->playlistList->clear();
foreach ( SpotifyPlaylistInfo* pl, playlists )
{
QListWidgetItem* item = new QListWidgetItem( pl->name, m_ui->playlistList );
item->setData( Qt::UserRole, QVariant::fromValue< SpotifyPlaylistInfo* >( pl ) );
item->setFlags( Qt::ItemIsUserCheckable | Qt::ItemIsSelectable | Qt::ItemIsEnabled );
item->setCheckState( pl->sync ? Qt::Checked : Qt::Unchecked );
}
}
void
SpotifyAccountConfig::doLogin()
{
m_ui->loginButton->setText( tr( "Logging in..." ) );
m_ui->loginButton->setEnabled( false );
m_playlistsLoading->fadeIn();
emit login( username(), password() );
}
void
SpotifyAccountConfig::loginResponse( bool success, const QString& msg )
{
if ( success )
{
m_ui->loginButton->setText( tr( "Logged in!" ) );
m_ui->loginButton->setEnabled( false );
}
else
{
setPlaylists( QList< SpotifyPlaylistInfo* >() );
m_playlistsLoading->fadeOut();
m_ui->loginButton->setText( tr( "Failed: %1" ).arg( msg ) );
m_ui->loginButton->setEnabled( true );
}
}
void
SpotifyAccountConfig::resetLoginButton()
{
m_ui->loginButton->setText( tr( "Log In" ) );
m_ui->loginButton->setEnabled( true );
}

View File

@ -0,0 +1,81 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk 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 3 of the License, or
* (at your option) any later version.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYACCOUNTCONFIG_H
#define SPOTIFYACCOUNTCONFIG_H
#include <QWidget>
#include <QVariantMap>
#include <QTimer>
class LoadingSpinner;
class QShowEvent;
namespace Ui
{
class SpotifyConfig;
}
namespace Tomahawk
{
namespace Accounts
{
class SpotifyAccount;
struct SpotifyPlaylistInfo;
class SpotifyAccountConfig : public QWidget
{
Q_OBJECT
public:
explicit SpotifyAccountConfig( SpotifyAccount* account );
QString username() const;
QString password() const;
bool highQuality() const;
bool deleteOnUnsync() const;
void setPlaylists( const QList< SpotifyPlaylistInfo* >& playlists );
void loadFromConfig();
void saveSettings();
void loginResponse( bool success, const QString& msg );
signals:
void login( const QString& username, const QString& pw );
public slots:
// void verifyResult( const QString& msgType, const QVariantMap& msg );
protected:
void showEvent( QShowEvent* event );
private slots:
void doLogin();
void resetLoginButton();
private:
Ui::SpotifyConfig* m_ui;
SpotifyAccount* m_account;
LoadingSpinner* m_playlistsLoading;
};
}
}
#endif // SPOTIFYACCOUNTCONFIG_H

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>397</width>
<height>487</height>
<width>419</width>
<height>454</height>
</rect>
</property>
<property name="sizePolicy">
@ -20,21 +20,33 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="icon">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>spotify-logo.png</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<property name="margin">
<number>4</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="icon">
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="../../../resources.qrc">:/data/images/spotify-logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="titleText">
<property name="font">
@ -45,7 +57,7 @@
</font>
</property>
<property name="text">
<string>Configure your Spotify credentials</string>
<string>Configure your Spotify account</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
@ -58,44 +70,61 @@
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="usernameLabel">
<property name="text">
<string>Username:</string>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="usernameEdit">
<property name="text">
<string>placeholderUsername</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="passwordEdit">
<property name="text">
<string>placeholderPw</string>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
<item row="0" column="0">
<widget class="QLabel" name="usernameLabel">
<property name="text">
<string>Username:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="usernameEdit">
<property name="text">
<string/>
</property>
<property name="placeholderText">
<string>Username or Facebook Email</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="passwordLabel">
<property name="text">
<string>Password:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="passwordEdit">
<property name="text">
<string/>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="loginButton">
<property name="text">
<string>Log In</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_5">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -113,15 +142,15 @@
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>High Quality Streaming</string>
<string>High Quality Streams</string>
</property>
<property name="checked">
<bool>STREAMING_DEFAULT</bool>
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -135,6 +164,23 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Spotify playlists to keep in sync:</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="playlistList"/>
</item>
<item>
<widget class="QCheckBox" name="deleteOnUnsync">
<property name="text">
<string>Delete Tomahawk playlist when removing synchronization</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@ -152,11 +198,35 @@
</item>
<item>
<widget class="QLabel" name="spotifyLogo">
<property name="maximumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap>spotifycore-logo.png</pixmap>
<pixmap resource="../../../resources.qrc">:/data/images/spotifycore-logo.png</pixmap>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>This product uses SPOTIFY(R) CORE but is not endorsed, certified or otherwise approved in any way by Spotify. Spotify is the registered trade mark of the Spotify Group.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
@ -175,25 +245,10 @@
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>This product uses SPOTIFY(R) CORE but is not endorsed, certified or otherwise approved in any way by Spotify. Spotify is the registered trade mark of the Spotify Group.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
<include location="../../../resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -0,0 +1,670 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk 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 3 of the License, or
* (at your option) any later version.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#include "SpotifyPlaylistUpdater.h"
#include "accounts/AccountManager.h"
#include "SpotifyAccount.h"
#include "utils/tomahawkutils.h"
#include <QMessageBox>
using namespace Tomahawk;
using namespace Accounts;
#ifndef ENABLE_HEADLESS
QPixmap* SpotifyPlaylistUpdater::s_typePixmap = 0;
#endif
Tomahawk::PlaylistUpdaterInterface*
SpotifyUpdaterFactory::create( const Tomahawk::playlist_ptr& pl, const QString &key )
{
if ( !m_account )
{
// Find the spotify account
foreach ( Account* account, AccountManager::instance()->accounts() )
{
if ( SpotifyAccount* spotify = qobject_cast< SpotifyAccount* >( account ) )
{
m_account = spotify;
break;
}
}
}
if ( m_account.isNull() )
{
qWarning() << "Found a spotify updater with no spotify account... ignoreing for now!!";
return 0;
}
// Register the updater with the account
const QString spotifyId = TomahawkSettings::instance()->value( QString( "%1/spotifyId" ).arg( key ) ).toString();
const QString latestRev = TomahawkSettings::instance()->value( QString( "%1/latestrev" ).arg( key ) ).toString();
const bool sync = TomahawkSettings::instance()->value( QString( "%1/sync" ).arg( key ) ).toBool();
Q_ASSERT( !spotifyId.isEmpty() );
SpotifyPlaylistUpdater* updater = new SpotifyPlaylistUpdater( m_account.data(), latestRev, spotifyId, pl );
updater->setSync( sync );
m_account.data()->registerUpdaterForPlaylist( spotifyId, updater );
return updater;
}
SpotifyPlaylistUpdater::SpotifyPlaylistUpdater( SpotifyAccount* acct, const QString& revid, const QString& spotifyId, const playlist_ptr& pl )
: PlaylistUpdaterInterface( pl )
, m_spotify( acct )
, m_latestRev( revid )
, m_spotifyId( spotifyId )
, m_blockUpdatesForNextRevision( false )
, m_sync( false )
{
init();
}
void
SpotifyPlaylistUpdater::init()
{
connect( playlist().data(), SIGNAL( tracksInserted( QList<Tomahawk::plentry_ptr>, int ) ), this, SLOT( tomahawkTracksInserted( QList<Tomahawk::plentry_ptr>, int ) ) );
connect( playlist().data(), SIGNAL( tracksRemoved( QList<Tomahawk::query_ptr> ) ), this, SLOT( tomahawkTracksRemoved( QList<Tomahawk::query_ptr> ) ) );
connect( playlist().data(), SIGNAL( tracksMoved( QList<Tomahawk::plentry_ptr>, int ) ), this, SLOT( tomahawkTracksMoved( QList<Tomahawk::plentry_ptr>, int ) ) );
connect( playlist().data(), SIGNAL( renamed( QString, QString ) ), this, SLOT( tomahawkPlaylistRenamed( QString, QString ) ) );
connect( playlist().data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistRevisionLoaded() ), Qt::QueuedConnection ); // Queued so that in playlist.cpp:443 we let the playlist clear its own queue first
// TODO reorders in a playlist
}
SpotifyPlaylistUpdater::~SpotifyPlaylistUpdater()
{
if ( !m_spotify.isNull() )
{
if ( m_sync )
{
QVariantMap msg;
msg[ "_msgtype" ] = "removeFromSyncList";
msg[ "playlistid" ] = m_spotifyId;
m_spotify.data()->sendMessage( msg );
m_spotify.data()->setSyncForPlaylist( m_spotifyId, false );
}
m_spotify.data()->unregisterUpdater( m_spotifyId );
}
}
void
SpotifyPlaylistUpdater::removeFromSettings( const QString& group ) const
{
TomahawkSettings::instance()->remove( QString( "%1/latestrev" ).arg( group ) );
TomahawkSettings::instance()->remove( QString( "%1/sync" ).arg( group ) );
TomahawkSettings::instance()->remove( QString( "%1/spotifyId" ).arg( group ) );
if ( m_sync )
{
if ( QThread::currentThread() != QApplication::instance()->thread() )
QMetaObject::invokeMethod( const_cast<SpotifyPlaylistUpdater*>(this), "checkDeleteDialog", Qt::BlockingQueuedConnection );
else
checkDeleteDialog();
}
}
void
SpotifyPlaylistUpdater::checkDeleteDialog() const
{
// Ask if we should delete the playlist on the spotify side as well
QMessageBox askDelete( QMessageBox::Question, tr( "Delete in Spotify?" ), tr( "Would you like to delete the corresponding Spotify playlist as well?" ), QMessageBox::Yes | QMessageBox::No, 0 );
int ret = askDelete.exec();
if ( ret == QMessageBox::Yes )
{
if ( m_spotify.isNull() )
return;
// User wants to delete it!
QVariantMap msg;
msg[ "_msgtype" ] = "deletePlaylist";
msg[ "playlistid" ] = m_spotifyId;
m_spotify.data()->sendMessage( msg );
}
}
void
SpotifyPlaylistUpdater::playlistRevisionLoaded()
{
if ( m_queuedOps.isEmpty() ) // nothing queued
return;
if ( playlist()->busy() ) // not ready yet, we'll get another revision loaded
return;
_detail::Closure* next = m_queuedOps.dequeue();
next->forceInvoke();
}
void
SpotifyPlaylistUpdater::saveToSettings( const QString& group ) const
{
TomahawkSettings::instance()->setValue( QString( "%1/latestrev" ).arg( group ), m_latestRev );
TomahawkSettings::instance()->setValue( QString( "%1/sync" ).arg( group ), m_sync );
TomahawkSettings::instance()->setValue( QString( "%1/spotifyId" ).arg( group ), m_spotifyId );
}
QString
SpotifyPlaylistUpdater::type() const
{
return "spotify";
}
#ifndef ENABLE_HEADLESS
QPixmap
SpotifyPlaylistUpdater::typeIcon() const
{
if ( !s_typePixmap )
{
QPixmap pm( RESPATH "images/spotify-logo.png" );
s_typePixmap = new QPixmap( pm.scaled( 32, 32, Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
}
if ( !m_sync )
return QPixmap();
return *s_typePixmap;
}
#endif
void
SpotifyPlaylistUpdater::setSync( bool sync )
{
if ( m_sync == sync )
return;
m_sync = sync;
emit changed();
}
bool
SpotifyPlaylistUpdater::sync() const
{
return m_sync;
}
void
SpotifyPlaylistUpdater::spotifyTracksAdded( const QVariantList& tracks, const QString& startPosId, const QString& newRev, const QString& oldRev )
{
if( playlist()->busy() )
{
// We might still be waiting for a add/remove tracks command to finish, so the entries we get here might be stale
// wait for any to be complete
m_queuedOps << NewClosure( 0, "", this, SLOT(spotifyTracksAdded(QVariantList, QString, QString, QString)), tracks, startPosId, newRev, oldRev );
return;
}
const QList< query_ptr > queries = variantToQueries( tracks );
qDebug() << Q_FUNC_INFO << "inserting tracks in middle of tomahawk playlist, from spotify command!" << tracks << startPosId << newRev << oldRev;
// Uh oh, dont' want to get out of sync!!
// Q_ASSERT( m_latestRev == oldRev );
// m_latestRev = newRev;
// Find the position of the track to insert from
int pos = -1;
QList< plentry_ptr > entries = playlist()->entries();
for ( int i = 0; i < entries.size(); i++ )
{
if ( entries[ i ]->annotation() == startPosId )
{
pos = i;
break;
}
}
pos++; // We found index of item before, so get index of new item.
if ( pos == -1 || pos > entries.size() )
pos = entries.size();
qDebug() << Q_FUNC_INFO << "inserting tracks at position:" << pos << "(playlist has current size:" << entries << ")";
m_blockUpdatesForNextRevision = true;
playlist()->insertEntries( queries, pos, playlist()->currentrevision() );
}
void
SpotifyPlaylistUpdater::spotifyTracksRemoved( const QVariantList& trackIds, const QString& newRev, const QString& oldRev )
{
if( playlist()->busy() )
{
// We might still be waiting for a add/remove tracks command to finish, so the entries we get here might be stale
// wait for any to be complete
m_queuedOps << NewClosure( 0, "", this, SLOT(spotifyTracksRemoved(QVariantList, QString, QString)), trackIds, newRev, oldRev );
return;
}
qDebug() << Q_FUNC_INFO << "remove tracks in middle of tomahawk playlist, from spotify command!" << trackIds << newRev << oldRev;
// Uh oh, dont' want to get out of sync!!
// Q_ASSERT( m_latestRev == oldRev );
// m_latestRev = newRev;
QList< plentry_ptr > entries = playlist()->entries();
// Collect list of tracks to remove (can't remove in-place as that might modify the indices)
QList<plentry_ptr> toRemove;
foreach( const QVariant trackIdV, trackIds )
{
const QString id = trackIdV.toString();
if ( id.isEmpty() )
{
qWarning() << Q_FUNC_INFO << "Tried to get track id to remove, but either couldn't convert to qstring:" << trackIdV;
continue;
}
foreach ( const plentry_ptr& entry, entries )
{
if ( entry->annotation() == id )
{
toRemove << entry;
break;
}
}
}
// Now remove them all
foreach( const plentry_ptr& torm, toRemove )
entries.removeAll( torm );
const int sizeDiff = playlist()->entries().size() - entries.size();
qDebug() << "We were asked to delete:" << trackIds.size() << "tracks from the playlist, and we deleted:" << sizeDiff;
if ( trackIds.size() != ( playlist()->entries().size() - entries.size() ) )
qWarning() << "========================= Failed to delete all the tracks we were asked for!! Didn't find some indicesss... ===================";
if ( sizeDiff > 0 )
{
// Won't get a tomahawkTracksInserted or tomahawkTracksRemoved slot called, no need to block
playlist()->createNewRevision( uuid(), playlist()->currentrevision(), entries );
}
}
void
SpotifyPlaylistUpdater::spotifyPlaylistRenamed( const QString& title, const QString& newRev, const QString& oldRev )
{
if( playlist()->busy() )
{
// We might still be waiting for a add/remove tracks command to finish, so the entries we get here might be stale
// wait for any to be complete
m_queuedOps << NewClosure( 0, "", this, SLOT(spotifyPlaylistRenamed(QString, QString, QString)), title, newRev, oldRev );
return;
}
Q_UNUSED( newRev );
Q_UNUSED( oldRev );
/// @note to self: should do some checking before trying to update
playlist()->rename( title );
}
void
SpotifyPlaylistUpdater::tomahawkPlaylistRenamed(const QString &newT, const QString &oldT)
{
qDebug() << Q_FUNC_INFO;
QVariantMap msg;
msg[ "_msgtype" ] = "playlistRenamed";
msg[ "oldrev" ] = m_latestRev;
msg[ "newTitle" ] = newT;
msg[ "oldTitle" ] = oldT;
msg[ "playlistid" ] = m_spotifyId;
m_spotify.data()->sendMessage( msg, this, "onPlaylistRename" );
}
void
SpotifyPlaylistUpdater::spotifyTracksMoved( const QVariantList& tracks, const QString& newStartPos, const QString& newRev, const QString& oldRev )
{
if( playlist()->busy() )
{
// We might still be waiting for a add/remove tracks command to finish, so the entries we get here might be stale
// wait for any to be complete
m_queuedOps << NewClosure( 0, "", this, SLOT(spotifyTracksMoved(QVariantList, QString, QString, QString)), tracks, newStartPos, newRev, oldRev );
return;
}
qDebug() << "Moving some tracks in a spotify-synced playlist, tracks:" << tracks << "to new startpos:" << newStartPos;
// Uh oh, dont' want to get out of sync!!
// Q_ASSERT( m_latestRev == oldRev );
// m_latestRev = newRev;
QList< plentry_ptr > entries = playlist()->entries();
QList< plentry_ptr > toMove;
for ( QList< plentry_ptr >::iterator iter = entries.begin(); iter != entries.end(); )
{
if ( (*iter)->annotation().isEmpty() )
continue;
if ( tracks.contains( (*iter)->annotation() ) )
{
toMove << *iter;
iter = entries.erase( iter );
continue;
}
++iter;
}
// Find the position of the track to insert from
if ( newStartPos.isEmpty() )
{
while ( !toMove.isEmpty() )
entries.prepend( toMove.takeLast() );
}
else
{
for ( QList< plentry_ptr >::iterator iter = entries.begin(); iter != entries.end(); ++iter )
{
if ( (*iter)->annotation() == newStartPos )
{
++iter;
while ( !toMove.isEmpty() )
{
qDebug() << "Adding moved track to playlist at pos (end:" << (iter == entries.end());
if ( iter != entries.end() )
qDebug() << (*iter)->query()->track() << (*iter)->query()->artist();
iter = entries.insert( iter, toMove.takeLast() );
}
break;
}
}
}
m_blockUpdatesForNextRevision = true;
playlist()->createNewRevision( uuid(), playlist()->currentrevision(), entries );
}
void
SpotifyPlaylistUpdater::tomahawkTracksInserted( const QList< plentry_ptr >& tracks, int pos )
{
if ( m_spotify.isNull() )
return;
if ( m_blockUpdatesForNextRevision )
{
qDebug() << "Ignoring tracks inserted message since we just did an insert ourselves!";
m_blockUpdatesForNextRevision = false;
return;
}
// Notify the resolver that we've updated
qDebug() << Q_FUNC_INFO << "updating spotify resolver with inserted tracks at:" << pos << tracks;
QVariantMap msg;
msg[ "_msgtype" ] = "addTracksToPlaylist";
msg[ "oldrev" ] = m_latestRev;
// Find the trackid of the nearest spotify track
QList< plentry_ptr > plTracks = playlist()->entries();
Q_ASSERT( pos-1 < plTracks.size() );
const QString startPos = nearestSpotifyTrack( plTracks, pos - 1 );
msg[ "startPosition" ] = startPos;
m_waitingForIds = tracks;
msg[ "playlistid" ] = m_spotifyId;
msg[ "tracks" ] = plentryToVariant( tracks );
m_spotify.data()->sendMessage( msg, this, "onTracksInsertedReturn" );
}
QString
SpotifyPlaylistUpdater::nearestSpotifyTrack( const QList< plentry_ptr >& entries, int pos )
{
for ( int i = pos; i >= 0; i-- )
{
if ( !entries[ i ]->annotation().isEmpty() && entries[ i ]->annotation().contains( "spotify:track") )
{
return entries[ i ]->annotation();
}
}
return QString();
}
QVariantList
SpotifyPlaylistUpdater::plentryToVariant( const QList< plentry_ptr >& entries )
{
QVariantList tracksJson;
foreach ( const plentry_ptr& ple, entries )
{
const query_ptr q = ple->query();
if ( q.isNull() )
{
qDebug() << "Got null query_ptr in plentry_ptr!!!" << ple;
continue;
}
tracksJson << queryToVariant( q );
}
return tracksJson;
}
void
SpotifyPlaylistUpdater::onTracksInsertedReturn( const QString& msgType, const QVariantMap& msg )
{
const bool success = msg.value( "success" ).toBool();
qDebug() << Q_FUNC_INFO << "GOT RETURN FOR tracksInserted call from spotify!" << msgType << msg << "Succeeded?" << success;
m_latestRev = msg.value( "revid" ).toString();
const QVariantList trackPositionsInserted = msg.value( "trackPosInserted" ).toList();
const QVariantList trackIdsInserted = msg.value( "trackIdInserted" ).toList();
Q_ASSERT( trackPositionsInserted.size() == trackIdsInserted.size() );
const QList< plentry_ptr > curEntries = playlist()->entries();
QList< plentry_ptr > changed;
for ( int i = 0; i < trackPositionsInserted.size(); i++ )
{
const QVariant posV = trackPositionsInserted[ i ];
bool ok;
const int pos = posV.toInt( &ok );
if ( !ok )
continue;
if ( pos < 0 || pos >= m_waitingForIds.size() )
{
qWarning() << Q_FUNC_INFO << "Got position that's not in the bounds of the tracks that we think we added... WTF?";
continue;
}
if ( !curEntries.contains( m_waitingForIds.at( pos ) ) )
{
qDebug() << Q_FUNC_INFO << "Got an id at a position for a plentry that's no longer in our playlist? WTF";
continue;
}
if ( i >= trackIdsInserted.size() )
{
qWarning() << Q_FUNC_INFO << "Help! Got more track positions than track IDS, wtf?";
continue;
}
qDebug() << "Setting annotation for track:" << m_waitingForIds[ pos ]->query()->track() << m_waitingForIds[ pos ]->query()->artist() << trackIdsInserted.at( i ).toString();
m_waitingForIds[ pos ]->setAnnotation( trackIdsInserted.at( i ).toString() );
changed << m_waitingForIds[ pos ];
}
m_waitingForIds.clear();
// Save our changes if we added some IDs
if ( changed.size() > 0 )
playlist()->updateEntries( uuid(), playlist()->currentrevision(), changed );
}
void
SpotifyPlaylistUpdater::tomahawkTracksRemoved( const QList< query_ptr >& tracks )
{
if ( m_spotify.isNull() )
return;
if ( m_blockUpdatesForNextRevision )
{
qDebug() << "Ignoring tracks removed message since we just did a remove ourselves!";
m_blockUpdatesForNextRevision = false;
return;
}
qDebug() << Q_FUNC_INFO << "updating spotify resolver with removed tracks:" << tracks;
QVariantMap msg;
msg[ "_msgtype" ] = "removeTracksFromPlaylist";
msg[ "playlistid" ] = m_spotifyId;
msg[ "oldrev" ] = m_latestRev;
msg[ "tracks" ] = queriesToVariant( tracks );
m_spotify.data()->sendMessage( msg, this, "onTracksRemovedReturn" );
}
void
SpotifyPlaylistUpdater::onTracksRemovedReturn( const QString& msgType, const QVariantMap& msg )
{
const bool success = msg.value( "success" ).toBool();
qDebug() << Q_FUNC_INFO << "GOT RETURN FOR tracksRemoved call from spotify!" << msgType << msg << "Succeeded?" << success;
m_latestRev = msg.value( "revid" ).toString();
}
void
SpotifyPlaylistUpdater::tomahawkTracksMoved( const QList< plentry_ptr >& tracks, int position )
{
if( playlist()->busy() )
{
// the playlist has had the new revision set, but it might not be finished, if it's not finished, playlist()->entries() still
// contains the *old* order, so we get the wrong data
m_queuedOps << NewClosure( 0, "", this, SLOT(tomahawkTracksMoved(QList<Tomahawk::plentry_ptr>,int)), tracks, position );
return;
}
qDebug() << Q_FUNC_INFO << "Got tracks moved at position:" << position;
foreach ( const plentry_ptr ple, tracks )
{
qDebug() << ple->query()->track() << ple->query()->artist();
}
qDebug() << Q_FUNC_INFO << "updating spotify resolver with moved tracks to:" << position;
QVariantMap msg;
msg[ "_msgtype" ] = "moveTracksInPlaylist";
msg[ "oldrev" ] = m_latestRev;
// Find the trackid of the nearest spotify track
QList< plentry_ptr > plTracks = playlist()->entries();
Q_ASSERT( position-1 < plTracks.size() );
QString startPos;
if ( position > 0 )
startPos = nearestSpotifyTrack( plTracks, position );
msg[ "startPosition" ] = startPos;
msg[ "playlistid" ] = m_spotifyId;
msg[ "tracks" ] = plentryToVariant( tracks );
m_spotify.data()->sendMessage( msg, this, "onTracksMovedReturn" );
}
void
SpotifyPlaylistUpdater::onTracksMovedReturn( const QString& msgType, const QVariantMap& msg )
{
const bool success = msg.value( "success" ).toBool();
qDebug() << Q_FUNC_INFO << "GOT RETURN FOR tracksMoved call from spotify!" << msgType << msg << "Succeeded?" << success;
m_latestRev = msg.value( "revid" ).toString();
}
QVariantList
SpotifyPlaylistUpdater::queriesToVariant( const QList< query_ptr >& queries )
{
QVariantList tracksJson;
foreach ( const query_ptr& q, queries )
{
QVariantMap m;
if ( q.isNull() )
continue;
tracksJson << queryToVariant( q );
}
return tracksJson;
}
QVariant
SpotifyPlaylistUpdater::queryToVariant( const query_ptr& query )
{
QVariantMap m;
m[ "track" ] = query->track();
m[ "artist" ] = query->artist();
m[ "album" ] = query->album();
if ( !query->property( "annotation" ).isNull() )
m[ "id" ] = query->property( "annotation" );
return m;
}
QList< query_ptr >
SpotifyPlaylistUpdater::variantToQueries( const QVariantList& list )
{
QList< query_ptr > queries;
foreach ( const QVariant& blob, list )
{
QVariantMap trackMap = blob.toMap();
const query_ptr q = Query::get( trackMap.value( "artist" ).toString(), trackMap.value( "track" ).toString(), trackMap.value( "album" ).toString(), uuid(), false );
if ( trackMap.contains( "id" ) )
q->setProperty( "annotation", trackMap.value( "id" ) );
queries << q;
}
return queries;
}

View File

@ -0,0 +1,118 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk 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 3 of the License, or
* (at your option) any later version.
*
* Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef SPOTIFYPLAYLISTUPDATER_H
#define SPOTIFYPLAYLISTUPDATER_H
#include "playlist/PlaylistUpdaterInterface.h"
#include "utils/closure.h"
#include <QQueue>
#include <QVariant>
namespace Tomahawk {
namespace Accounts {
class SpotifyAccount;
}
}
class SpotifyPlaylistUpdater : public Tomahawk::PlaylistUpdaterInterface
{
Q_OBJECT
friend class Tomahawk::Accounts::SpotifyAccount;
public:
SpotifyPlaylistUpdater( Tomahawk::Accounts::SpotifyAccount* acct, const QString& revid, const QString& spotifyId, const Tomahawk::playlist_ptr& pl );
virtual ~SpotifyPlaylistUpdater();
virtual QString type() const;
virtual void updateNow() {}
#ifndef ENABLE_HEADLESS
virtual QWidget* configurationWidget() const { return 0; }
virtual QPixmap typeIcon() const;
#endif
bool sync() const;
void setSync( bool sync );
QString spotifyId() const { return m_spotifyId; }
public slots:
/// Spotify callbacks when we are directly instructed from the resolver
void spotifyTracksAdded( const QVariantList& tracks, const QString& startPosId, const QString& newRev, const QString& oldRev );
void spotifyTracksRemoved( const QVariantList& tracks, const QString& newRev, const QString& oldRev );
void spotifyTracksMoved( const QVariantList& tracks, const QString& newStartPos, const QString& newRev, const QString& oldRev );
void spotifyPlaylistRenamed( const QString& title, const QString& newRev, const QString& oldRev );
void tomahawkTracksInserted( const QList<Tomahawk::plentry_ptr>& ,int );
void tomahawkTracksRemoved( const QList<Tomahawk::query_ptr>& );
void tomahawkTracksMoved( const QList<Tomahawk::plentry_ptr>& ,int );
void tomahawkPlaylistRenamed( const QString&, const QString& );
protected:
virtual void removeFromSettings(const QString& group) const;
virtual void saveToSettings(const QString& group) const;
private slots:
// SpotifyResolver message handlers, all take msgtype, msg as argument
void onTracksInsertedReturn( const QString& msgType, const QVariantMap& msg );
void onTracksRemovedReturn( const QString& msgType, const QVariantMap& msg );
void onTracksMovedReturn( const QString& msgType, const QVariantMap& msg );
void checkDeleteDialog() const;
void playlistRevisionLoaded();
private:
void init();
/// Finds the nearest spotify id from pos to the beginning of the playlist
QString nearestSpotifyTrack( const QList< Tomahawk::plentry_ptr >& entries, int pos );
QVariantList plentryToVariant( const QList< Tomahawk::plentry_ptr >& entries );
static QVariantList queriesToVariant( const QList< Tomahawk::query_ptr >& queries );
static QVariant queryToVariant( const Tomahawk::query_ptr& query );
static QList< Tomahawk::query_ptr > variantToQueries( const QVariantList& list );
QWeakPointer<Tomahawk::Accounts::SpotifyAccount> m_spotify;
QString m_latestRev, m_spotifyId;
QList< Tomahawk::plentry_ptr > m_waitingForIds;
bool m_blockUpdatesForNextRevision;
bool m_sync;
QQueue<_detail::Closure*> m_queuedOps;
#ifndef ENABLE_HEADLESS
static QPixmap* s_typePixmap;
#endif
};
class SpotifyUpdaterFactory : public Tomahawk::PlaylistUpdaterFactory
{
public:
SpotifyUpdaterFactory() {}
virtual Tomahawk::PlaylistUpdaterInterface* create( const Tomahawk::playlist_ptr& pl, const QString& key );
virtual QString type() const { return "spotify"; }
private:
QWeakPointer<Tomahawk::Accounts::SpotifyAccount> m_account;
};
#endif // SPOTIFYPLAYLISTUPDATER_H

View File

@ -42,7 +42,7 @@ XmppAccountFactory::createAccount( const QString& accountId )
XmppAccount::XmppAccount( const QString &accountId )
: Account( accountId )
{
setAccountServiceName( "XMPP (Jabber)" );
setAccountServiceName( "Jabber (XMPP)" );
setTypes( SipType );
m_configWidget = QWeakPointer< QWidget >( new XmppConfigWidget( this, 0 ) );

View File

@ -47,7 +47,7 @@ public:
XmppAccountFactory() {}
virtual ~XmppAccountFactory() {}
QString prettyName() const { return "XMPP (Jabber)"; }
QString prettyName() const { return "Jabber (XMPP)"; }
QString description() const { return tr( "Log on to your Jabber/XMPP account to connect to your friends" ); }
QString factoryId() const { return "xmppaccount"; }
QPixmap icon() const { return QPixmap( ":/xmpp-icon.png" ); }

View File

@ -121,6 +121,7 @@ set( libGuiSources
utils/tomahawkutilsgui.cpp
utils/closure.cpp
utils/PixmapDelegateFader.cpp
utils/SmartPointerList.h
widgets/animatedcounterlabel.cpp
widgets/checkdirtree.cpp

View File

@ -205,4 +205,4 @@ Account::types() const
}
}
}

View File

@ -2,6 +2,7 @@
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -37,6 +38,14 @@ ActionCollection::ActionCollection( QObject *parent )
}
ActionCollection::~ActionCollection()
{
s_instance = 0;
foreach( QString key, m_actionCollection.keys() )
delete m_actionCollection[ key ];
}
void
ActionCollection::initActions()
{
@ -76,18 +85,54 @@ ActionCollection::initActions()
}
ActionCollection::~ActionCollection()
void
ActionCollection::addAction( ActionCollection::ActionDestination category, QAction* action, QObject* notify )
{
s_instance = 0;
foreach( QString key, m_actionCollection.keys() )
delete m_actionCollection[ key ];
QList< QAction* > actions = m_categoryActions.value( category );
actions.append( action );
m_categoryActions[ category ] = actions;
if ( notify )
m_actionNotifiers[ action ] = notify;
}
QAction*
ActionCollection::getAction( const QString& name )
{
return m_actionCollection.contains( name ) ? m_actionCollection[ name ] : 0;
return m_actionCollection.value( name, 0 );
}
QObject*
ActionCollection::actionNotifier( QAction* action )
{
return m_actionNotifiers.value( action, 0 );
}
QList< QAction* >
ActionCollection::getAction( ActionCollection::ActionDestination category )
{
return m_categoryActions.value( category );
}
void
ActionCollection::removeAction( QAction* action )
{
removeAction( action, LocalPlaylists );
}
void
ActionCollection::removeAction( QAction* action, ActionCollection::ActionDestination category )
{
QList< QAction* > actions = m_categoryActions.value( category );
actions.removeAll( action );
m_categoryActions[ category ] = actions;
m_actionNotifiers.remove( action );
}

View File

@ -1,7 +1,8 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>>
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -29,6 +30,12 @@ class DLLEXPORT ActionCollection : public QObject
Q_OBJECT
public:
// Categories for custom-registered actions
enum ActionDestination {
// Tracks, TODO
LocalPlaylists = 0
};
static ActionCollection* instance();
ActionCollection( QObject *parent);
@ -37,6 +44,34 @@ public:
void initActions();
QAction* getAction( const QString& name );
QList< QAction* > getAction( ActionDestination category );
QObject* actionNotifier( QAction* );
/**
* Add an action for a specific category. The action will show up
* where the relevant category is displayed.
*
* e.g. if you register a Playlist action, it will be shown when
* there is a context menu shown for a playlist.
*
* When the QAction* is shown, it will have a "payload" property that is set
* to the <specific type> that is being shown.
*
* Additionally you can pass a QObject* that will be notified before the given
* action is shown. The slot "aboutToShow( QAction*, <specific type> ) will be called,
*
*
* <specific type> corresponds to the category: playlist_ptr for Playlists, etc.
*
* The Action Collection takes ownership of the action. It's time to let go.
*/
void addAction( ActionDestination category, QAction* action, QObject* notify = 0 );
/**
* Remove an action from one or all specific categories
*/
void removeAction( QAction* action );
void removeAction( QAction* action, ActionDestination category );
public slots:
void togglePrivateListeningMode();
@ -48,6 +83,8 @@ private:
static ActionCollection* s_instance;
QHash< QString, QAction* > m_actionCollection;
QHash< ActionDestination, QList< QAction* > > m_categoryActions;
QHash< QAction*, QObject* > m_actionNotifiers;
};
#endif

View File

@ -77,6 +77,7 @@ DatabaseCommand_LoadPlaylistEntries::generateEntries( DatabaseImpl* dbi )
Tomahawk::query_ptr q = Tomahawk::Query::get( query.value( 2 ).toString(), query.value( 1 ).toString(), query.value( 3 ).toString() );
q->setResultHint( query.value( 8 ).toString() );
q->setProperty( "annotation", e->annotation() );
e->setQuery( q );
m_entrymap.insert( e->guid(), e );

View File

@ -43,6 +43,7 @@ DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision(
, m_oldrev( oldrev )
, m_addedentries( addedentries )
, m_entries( entries )
, m_metadataUpdate( false )
{
Q_ASSERT( !newrev.isEmpty() );
m_localOnly = ( newrev == oldrev );
@ -57,6 +58,33 @@ DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision(
}
DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision(
const source_ptr& s,
const QString& playlistguid,
const QString& newrev,
const QString& oldrev,
const QStringList& orderedguids,
const QList<plentry_ptr>& entriesToUpdate )
: DatabaseCommandLoggable( s )
, m_applied( false )
, m_newrev( newrev )
, m_oldrev( oldrev )
, m_entries( entriesToUpdate )
, m_metadataUpdate( true )
{
Q_ASSERT( !newrev.isEmpty() );
m_localOnly = false;
QVariantList tmp;
foreach( const QString& s, orderedguids )
tmp << s;
setOrderedguids( tmp );
setPlaylistguid( playlistguid );
}
void
DatabaseCommand_SetPlaylistRevision::postCommitHook()
{
@ -105,7 +133,7 @@ DatabaseCommand_SetPlaylistRevision::exec( DatabaseImpl* lib )
else
{
tDebug() << "Playlist:" << m_playlistguid << m_currentRevision << source()->friendlyName() << source()->id();
throw "No such playlist, WTF?";
Q_ASSERT_X( false, "DatabaseCommand_SetPlaylistRevision::exec", "No such playlist, WTF?" );
return;
}
@ -132,6 +160,26 @@ DatabaseCommand_SetPlaylistRevision::exec( DatabaseImpl* lib )
return;
}
else if ( m_metadataUpdate )
{
QString sql = "UPDATE playlist_item SET trackname = ?, artistname = ?, albumname = ?, annotation = ?, duration = ?, addedon = ?, addedby = ? WHERE guid = ?";
adde.prepare( sql );
foreach( const plentry_ptr& e, m_entries )
{
adde.bindValue( 0, e->query()->track() );
adde.bindValue( 1, e->query()->artist() );
adde.bindValue( 2, e->query()->album() );
adde.bindValue( 3, e->annotation() );
adde.bindValue( 4, (int) e->duration() );
adde.bindValue( 5, e->lastmodified() );
adde.bindValue( 6, source()->isLocal() ? QVariant(QVariant::Int) : source()->id() );
adde.bindValue( 7, e->guid() );
adde.exec();
}
}
else
{
QString sql = "INSERT INTO playlist_item( guid, playlist, trackname, artistname, albumname, "
@ -142,6 +190,8 @@ DatabaseCommand_SetPlaylistRevision::exec( DatabaseImpl* lib )
qDebug() << "Num new playlist_items to add:" << m_addedentries.length();
foreach( const plentry_ptr& e, m_addedentries )
{
// qDebug() << "Adding:" << e->guid() << e->query()->track() << e->query()->artist();
m_addedmap.insert( e->guid(), e ); // needed in postcommithook
QString resultHint;

View File

@ -35,15 +35,18 @@ Q_PROPERTY( QString newrev READ newrev WRITE setNewrev )
Q_PROPERTY( QString oldrev READ oldrev WRITE setOldrev )
Q_PROPERTY( QVariantList orderedguids READ orderedguids WRITE setOrderedguids )
Q_PROPERTY( QVariantList addedentries READ addedentriesV WRITE setAddedentriesV )
Q_PROPERTY( bool metadataUpdate READ metadataUpdate WRITE setMetadataUpdate )
public:
explicit DatabaseCommand_SetPlaylistRevision( QObject* parent = 0 )
: DatabaseCommandLoggable( parent )
, m_applied( false )
, m_localOnly( false )
, m_metadataUpdate( false )
{}
explicit DatabaseCommand_SetPlaylistRevision( const source_ptr& s,
// Constructor for inserting or removing entries
DatabaseCommand_SetPlaylistRevision( const source_ptr& s,
const QString& playlistguid,
const QString& newrev,
const QString& oldrev,
@ -51,6 +54,15 @@ public:
const QList<Tomahawk::plentry_ptr>& addedentries,
const QList<Tomahawk::plentry_ptr>& entries );
// constructor for updating metadata only
DatabaseCommand_SetPlaylistRevision( const source_ptr& s,
const QString& playlistguid,
const QString& newrev,
const QString& oldrev,
const QStringList& orderedguids,
const QList<Tomahawk::plentry_ptr>& entriesToUpdate );
QString commandname() const { return "setplaylistrevision"; }
virtual void exec( DatabaseImpl* lib );
@ -89,6 +101,8 @@ public:
QString newrev() const { return m_newrev; }
QString oldrev() const { return m_oldrev; }
QString playlistguid() const { return m_playlistguid; }
bool metadataUpdate() const { return m_metadataUpdate; }
void setMetadataUpdate( bool metadataUpdate ) { m_metadataUpdate = metadataUpdate; }
void setOrderedguids( const QVariantList& l ) { m_orderedguids = l; }
QVariantList orderedguids() const { return m_orderedguids; }
@ -106,7 +120,7 @@ private:
QVariantList m_orderedguids;
QList<Tomahawk::plentry_ptr> m_addedentries, m_entries;
bool m_localOnly;
bool m_localOnly, m_metadataUpdate;
};
#endif // DATABASECOMMAND_SETPLAYLISTREVISION_H

View File

@ -1097,7 +1097,10 @@ GlobalActionManager::doBookmark( const playlist_ptr& pl, const query_ptr& q )
e->setDuration( 0 );
e->setLastmodified( 0 );
e->setAnnotation( "" ); // FIXME
QString annotation = "";
if ( !q->property( "annotation" ).toString().isEmpty() )
annotation = q->property( "annotation" ).toString();
e->setAnnotation( annotation );
e->setQuery( q );
pl->createNewRevision( uuid(), pl->currentrevision(), QList< plentry_ptr >( pl->entries() ) << e );

View File

@ -40,6 +40,8 @@ using namespace Tomahawk;
InfoBar::InfoBar( QWidget* parent )
: QWidget( parent )
, ui( new Ui::InfoBar )
, m_updaterInterface( 0 )
, m_updaterConfiguration( 0 )
, m_queryLabel( 0 )
{
ui->setupUi( this );
@ -59,12 +61,12 @@ InfoBar::InfoBar( QWidget* parent )
regFont.setPixelSize( 11 );
ui->longDescriptionLabel->setFont( regFont );
QPalette whitePal = ui->captionLabel->palette();
whitePal.setColor( QPalette::Foreground, Qt::white );
m_whitePal = ui->captionLabel->palette();
m_whitePal.setColor( QPalette::Foreground, Qt::white );
ui->captionLabel->setPalette( whitePal );
ui->descriptionLabel->setPalette( whitePal );
ui->longDescriptionLabel->setPalette( whitePal );
ui->captionLabel->setPalette( m_whitePal );
ui->descriptionLabel->setPalette( m_whitePal );
ui->longDescriptionLabel->setPalette( m_whitePal );
ui->captionLabel->setMargin( 6 );
ui->descriptionLabel->setMargin( 6 );
@ -83,14 +85,6 @@ InfoBar::InfoBar( QWidget* parent )
m_queryLabel->hide();
connect( m_queryLabel, SIGNAL( clickedArtist() ), this, SLOT( artistClicked() ) );
m_autoUpdate = new QCheckBox( this );
m_autoUpdate->setText( tr( "Automatically update" ) );
m_autoUpdate->setLayoutDirection( Qt::RightToLeft );
m_autoUpdate->setPalette( whitePal );
connect( m_autoUpdate, SIGNAL( toggled( bool ) ), this, SIGNAL( autoUpdateChanged( bool ) ) );
ui->horizontalLayout->addWidget( m_autoUpdate );
m_searchWidget = new QSearchField( this );
m_searchWidget->setPlaceholderText( tr( "Filter..." ) );
m_searchWidget->setMinimumWidth( 180 );
@ -106,7 +100,6 @@ InfoBar::InfoBar( QWidget* parent )
createTile();
connect( ViewManager::instance(), SIGNAL( filterAvailable( bool ) ), SLOT( setFilterAvailable( bool ) ) );
connect( ViewManager::instance(), SIGNAL( autoUpdateAvailable( bool ) ), SLOT( setAutoUpdateAvailable( bool ) ) );
}
@ -207,13 +200,36 @@ InfoBar::setFilterAvailable( bool b )
m_searchWidget->setVisible( b );
}
void
InfoBar::setAutoUpdateAvailable( bool b )
{
if ( b )
m_autoUpdate->setChecked( ViewManager::instance()->currentPage()->autoUpdate() );
m_autoUpdate->setVisible( b );
void
InfoBar::setAutoUpdateInterface( PlaylistUpdaterInterface *interface )
{
if ( m_updaterConfiguration )
m_updaterConfiguration->hide();
if ( m_updaterConfiguration && ( interface ? (m_updaterConfiguration != interface->configurationWidget()) : true ) )
ui->horizontalLayout->removeWidget( m_updaterConfiguration );
m_updaterInterface = interface;
m_updaterConfiguration = interface ? interface->configurationWidget() : 0;
if ( !m_updaterInterface || !m_updaterConfiguration )
return;
m_updaterConfiguration->setPalette( m_whitePal );
int insertIdx = -1; // Ugh, no indexOf for QSpacerItem*
for ( int i = 0; i < ui->horizontalLayout->count(); i++ )
{
if ( ui->horizontalLayout->itemAt( i )->spacerItem() == ui->horizontalSpacer_4 )
{
insertIdx = i;
break;
}
}
insertIdx++;
ui->horizontalLayout->insertWidget( insertIdx, m_updaterConfiguration );
m_updaterConfiguration->show();
}

View File

@ -35,6 +35,11 @@ namespace Ui
class InfoBar;
}
namespace Tomahawk
{
class PlaylistUpdaterInterface;
}
class DLLEXPORT InfoBar : public QWidget
{
Q_OBJECT
@ -57,10 +62,9 @@ public slots:
void setFilter( const QString& filter );
void setFilterAvailable( bool b );
void setAutoUpdateAvailable( bool b );
void setAutoUpdateInterface( Tomahawk::PlaylistUpdaterInterface* interface );
signals:
void filterTextChanged( const QString& filter );
void autoUpdateChanged( bool checked );
protected:
void changeEvent( QEvent* e );
@ -76,9 +80,12 @@ private:
Ui::InfoBar* ui;
QPixmap m_bgTile;
QPalette m_whitePal;
Tomahawk::PlaylistUpdaterInterface* m_updaterInterface;
QWidget* m_updaterConfiguration;
QSearchField* m_searchWidget;
QCheckBox* m_autoUpdate;
QueryLabel* m_queryLabel;
};

View File

@ -1,6 +1,7 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
@ -95,7 +96,6 @@ PlaylistEntry::setLastSource( source_ptr s )
Playlist::Playlist( const source_ptr& author )
: m_source( author )
, m_lastmodified( 0 )
, m_updater( 0 )
{
}
@ -120,7 +120,6 @@ Playlist::Playlist( const source_ptr& src,
, m_lastmodified( lastmod )
, m_createdOn( createdOn )
, m_shared( shared )
, m_updater( 0 )
{
init();
}
@ -143,7 +142,6 @@ Playlist::Playlist( const source_ptr& author,
, m_createdOn( 0 ) // will be set by db command
, m_shared( shared )
, m_initEntries( entries )
, m_updater( 0 )
{
init();
}
@ -180,7 +178,10 @@ Playlist::create( const source_ptr& author,
p->setGuid( uuid() );
p->setDuration( query->duration() );
p->setLastmodified( 0 );
p->setAnnotation( "" );
QString annotation = "";
if ( !query->property( "annotation" ).toString().isEmpty() )
annotation = query->property( "annotation" ).toString();
p->setAnnotation( annotation );
p->setQuery( query );
entries << p;
@ -244,6 +245,18 @@ Playlist::rename( const QString& title )
Database::instance()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );
}
void
Playlist::setTitle( const QString& title )
{
if ( title == m_title )
return;
const QString oldTitle = m_title;
m_title = title;
emit changed();
emit renamed( m_title, oldTitle );
}
void
Playlist::reportCreated( const playlist_ptr& self )
@ -257,8 +270,8 @@ void
Playlist::reportDeleted( const Tomahawk::playlist_ptr& self )
{
Q_ASSERT( self.data() == this );
if ( m_updater )
m_updater->remove();
if ( !m_updater.isNull() )
m_updater.data()->remove();
m_deleted = true;
m_source->collection()->deletePlaylist( self );
@ -266,6 +279,28 @@ Playlist::reportDeleted( const Tomahawk::playlist_ptr& self )
emit deleted( self );
}
void
Playlist::setUpdater( PlaylistUpdaterInterface* pluinterface )
{
if ( !m_updater.isNull() )
disconnect( m_updater.data(), SIGNAL( changed() ), this, SIGNAL( changed() ) );
m_updater = QWeakPointer< PlaylistUpdaterInterface >( pluinterface );
connect( m_updater.data(), SIGNAL( changed() ), this, SIGNAL( changed() ), Qt::UniqueConnection );
connect( m_updater.data(), SIGNAL( destroyed( QObject* ) ), this, SLOT( updaterDestroyed() ), Qt::QueuedConnection );
emit changed();
}
void
Playlist::updaterDestroyed()
{
m_updater.clear();
emit changed();
}
void
Playlist::loadRevision( const QString& rev )
@ -312,8 +347,15 @@ Playlist::createNewRevision( const QString& newrev, const QString& oldrev, const
// calc list of newly added entries:
QList<plentry_ptr> added = newEntries( entries );
QStringList orderedguids;
qDebug() << "Inserting ordered GUIDs:";
foreach( const plentry_ptr& p, entries )
{
qDebug() << p->guid() << p->query()->track() << p->query()->artist();
orderedguids << p->guid();
}
foreach( const plentry_ptr& p, added )
qDebug() << p->guid();
// source making the change (local user in this case)
source_ptr author = SourceList::instance()->getLocal();
@ -331,6 +373,38 @@ Playlist::createNewRevision( const QString& newrev, const QString& oldrev, const
}
void
Playlist::updateEntries( const QString& newrev, const QString& oldrev, const QList< plentry_ptr >& entries )
{
tDebug() << Q_FUNC_INFO << newrev << oldrev << entries.count();
Q_ASSERT( m_source->isLocal() || newrev == oldrev );
if ( busy() )
{
m_updateQueue.enqueue( RevisionQueueItem( newrev, oldrev, entries, oldrev == currentrevision() ) );
return;
}
if ( newrev != oldrev )
setBusy( true );
QStringList orderedguids;
foreach( const plentry_ptr& p, m_entries )
orderedguids << p->guid();
qDebug() << "Updating playlist metadata:" << entries;
DatabaseCommand_SetPlaylistRevision* cmd =
new DatabaseCommand_SetPlaylistRevision( SourceList::instance()->getLocal(),
guid(),
newrev,
oldrev,
orderedguids,
entries );
Database::instance()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
}
// private, called when we loadRevision, or by our friend class DatabaseCommand_SetPlaylistRevision
// used to save new revision data (either originating locally, or via network msg for syncing)
void
@ -395,20 +469,24 @@ Playlist::setNewRevision( const QString& rev,
// existing ones, and the ones that have been added
QMap<QString, plentry_ptr> entriesmap;
foreach ( const plentry_ptr& p, m_entries )
{
qDebug() << p->guid() << p->query()->track() << p->query()->artist();
entriesmap.insert( p->guid(), p );
}
// re-build m_entries from neworderedguids. plentries come either from the old m_entries OR addedmap.
m_entries.clear();
QList<plentry_ptr> entries;
foreach ( const QString& id, neworderedguids )
{
if ( entriesmap.contains( id ) )
{
entries.append( entriesmap.value( id ) );
m_entries.append( entriesmap.value( id ) );
}
else if ( addedmap.contains( id ) )
{
entries.append( addedmap.value( id ) );
if ( is_newest_rev )
m_entries.append( addedmap.value( id ) );
m_entries.append( addedmap.value( id ) );
}
else
{
@ -426,35 +504,8 @@ Playlist::setNewRevision( const QString& rev,
PlaylistRevision pr;
pr.oldrevisionguid = m_currentrevision;
pr.revisionguid = rev;
// entries that have been removed:
QSet<QString> removedguids = oldorderedguids.toSet().subtract( neworderedguids.toSet() );
//qDebug() << "Removedguids:" << removedguids << "oldorederedguids" << oldorderedguids << "newog" << neworderedguids;
foreach ( QString remid, removedguids )
{
// NB: entriesmap will contain old/removed entries only if the removal was done
// in the same session - after a restart, history is not in memory.
if ( entriesmap.contains( remid ) )
{
pr.removed << entriesmap.value( remid );
if ( is_newest_rev )
{
//qDebug() << "Removing from m_entries" << remid;
for ( int k = 0 ; k < m_entries.length(); ++k )
{
if ( m_entries.at( k )->guid() == remid )
{
//qDebug() << "removed at" << k;
m_entries.removeAt( k );
break;
}
}
}
}
}
pr.added = addedmap.values();
pr.newlist = entries;
pr.newlist = m_entries;
return pr;
}
@ -514,8 +565,44 @@ Playlist::addEntries( const QList<query_ptr>& queries, const QString& oldrev )
{
QList<plentry_ptr> el = entriesFromQueries( queries );
const int prevSize = m_entries.size();
QString newrev = uuid();
createNewRevision( newrev, oldrev, el );
// We are appending at end, so notify listeners.
// PlaylistModel also emits during appends, but since we call
// createNewRevision, it reloads instead of appends.
const QList<plentry_ptr> added = el.mid( prevSize );
qDebug() << "Playlist got" << queries.size() << "tracks added, emitting tracksInserted with:" << added.size() << "at pos:" << prevSize - 1;
emit tracksInserted( added, prevSize );
}
void
Playlist::insertEntries( const QList< query_ptr >& queries, const int position, const QString& oldrev )
{
QList<plentry_ptr> toInsert = entriesFromQueries( queries, true );
QList<plentry_ptr> entries = m_entries;
Q_ASSERT( position <= m_entries.size() );
if ( position > m_entries.size() )
{
qWarning() << "ERROR trying to insert tracks past end of playlist! Appending!!";
addEntries( queries, oldrev );
return;
}
for ( int i = toInsert.size()-1; i >= 0; --i )
entries.insert( position, toInsert.at(i) );
createNewRevision( uuid(), oldrev, entries );
// We are appending at end, so notify listeners.
// PlaylistModel also emits during appends, but since we call
// createNewRevision, it reloads instead of appends.
qDebug() << "Playlist got" << toInsert.size() << "tracks added, emitting tracksInserted at pos:" << position;
emit tracksInserted( toInsert, position );
}
@ -537,7 +624,10 @@ Playlist::entriesFromQueries( const QList<Tomahawk::query_ptr>& queries, bool cl
e->setDuration( 0 );
e->setLastmodified( 0 );
e->setAnnotation( "" ); // FIXME
QString annotation = "";
if ( !query->property( "annotation" ).toString().isEmpty() )
annotation = query->property( "annotation" ).toString();
e->setAnnotation( annotation ); // FIXME
e->setQuery( query );
el << e;
@ -592,6 +682,23 @@ Playlist::checkRevisionQueue()
}
createNewRevision( item.newRev, item.oldRev, item.entries );
}
if ( !m_updateQueue.isEmpty() )
{
RevisionQueueItem item = m_updateQueue.dequeue();
if ( item.oldRev != currentrevision() && item.applyToTip )
{
// this was applied to the then-latest, but the already-running operation changed it so it's out of date now. fix it
if ( item.oldRev == item.newRev )
{
checkRevisionQueue();
return;
}
item.oldRev = currentrevision();
}
updateEntries( item.newRev, item.oldRev, item.entries );
}
}

View File

@ -1,6 +1,7 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2011920-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
@ -38,6 +39,8 @@ class DatabaseCommand_LoadAllPlaylists;
class DatabaseCommand_LoadAllSortedPlaylists;
class DatabaseCommand_SetPlaylistRevision;
class DatabaseCommand_CreatePlaylist;
class PlaylistModel;
namespace Tomahawk
{
@ -117,20 +120,21 @@ public:
class DLLEXPORT Playlist : public QObject
{
Q_OBJECT
Q_PROPERTY( QString guid READ guid WRITE setGuid )
Q_PROPERTY( QString currentrevision READ currentrevision WRITE setCurrentrevision )
Q_PROPERTY( QString title READ title WRITE setTitle )
Q_PROPERTY( QString info READ info WRITE setInfo )
Q_PROPERTY( QString creator READ creator WRITE setCreator )
Q_PROPERTY( unsigned int createdon READ createdOn WRITE setCreatedOn )
Q_PROPERTY( bool shared READ shared WRITE setShared )
Q_OBJECT
Q_PROPERTY( QString guid READ guid WRITE setGuid )
Q_PROPERTY( QString currentrevision READ currentrevision WRITE setCurrentrevision )
Q_PROPERTY( QString title READ title WRITE setTitle )
Q_PROPERTY( QString info READ info WRITE setInfo )
Q_PROPERTY( QString creator READ creator WRITE setCreator )
Q_PROPERTY( unsigned int createdon READ createdOn WRITE setCreatedOn )
Q_PROPERTY( bool shared READ shared WRITE setShared )
friend class ::DatabaseCommand_LoadAllPlaylists;
friend class ::DatabaseCommand_LoadAllSortedPlaylists;
friend class ::DatabaseCommand_SetPlaylistRevision;
friend class ::DatabaseCommand_CreatePlaylist;
friend class DynamicPlaylist;
friend class ::PlaylistModel;
public:
virtual ~Playlist();
@ -166,6 +170,7 @@ public:
const QList< plentry_ptr >& entries() { return m_entries; }
virtual void addEntry( const Tomahawk::query_ptr& query, const QString& oldrev );
virtual void addEntries( const QList<Tomahawk::query_ptr>& queries, const QString& oldrev );
virtual void insertEntries( const QList<Tomahawk::query_ptr>& queries, const int position, const QString& oldrev );
// <IGNORE hack="true">
// these need to exist and be public for the json serialization stuff
@ -173,18 +178,18 @@ public:
// maybe friend QObjectHelper and make them private?
explicit Playlist( const source_ptr& author );
void setCurrentrevision( const QString& s ) { m_currentrevision = s; }
void setTitle( const QString& s ) { m_title = s; emit changed(); }
void setInfo( const QString& s ) { m_info = s; }
void setCreator( const QString& s ) { m_creator = s; }
void setGuid( const QString& s ) { m_guid = s; }
void setShared( bool b ) { m_shared = b; }
void setCreatedOn( uint createdOn ) { m_createdOn = createdOn; }
void setTitle( const QString& s );
// </IGNORE>
QList<plentry_ptr> entriesFromQueries( const QList<Tomahawk::query_ptr>& queries, bool clearFirst = false );
void setUpdater( PlaylistUpdaterInterface* pluinterface ) { m_updater = pluinterface; }
PlaylistUpdaterInterface* updater() const { return m_updater; }
void setUpdater( PlaylistUpdaterInterface* pluinterface );
PlaylistUpdaterInterface* updater() const { return m_updater.data(); }
Tomahawk::playlistinterface_ptr playlistInterface();
@ -197,6 +202,7 @@ signals:
/// renamed etc.
void changed();
void renamed( const QString& newTitle, const QString& oldTitle );
/**
* delete command is scheduled but not completed. Do not call remove() again once this
@ -207,10 +213,27 @@ signals:
/// was deleted, eh?
void deleted( const Tomahawk::playlist_ptr& pl );
/// Notification for tracks being inserted at a specific point
/// Contiguous range from startPosition
void tracksInserted( const QList< Tomahawk::plentry_ptr >& tracks, int startPosition );
/// Notification for tracks being removed from playlist
void tracksRemoved( const QList< Tomahawk::query_ptr >& tracks );
/// Notification for tracks being moved in a playlist. List is of new tracks, and new position of first track
/// Contiguous range from startPosition
void tracksMoved( const QList< Tomahawk::plentry_ptr >& tracks, int startPosition );
public slots:
// want to update the playlist from the model?
// generate a newrev using uuid() and call this:
void createNewRevision( const QString& newrev, const QString& oldrev, const QList< plentry_ptr >& entries );
// Want to update some metadata of a plentry_ptr? this gets you a new revision too.
// entries should be <= entries(), with changed metadata.
void updateEntries( const QString& newrev, const QString& oldrev, const QList< plentry_ptr >& entries );
void reportCreated( const Tomahawk::playlist_ptr& self );
void reportDeleted( const Tomahawk::playlist_ptr& self );
@ -255,6 +278,7 @@ protected:
private slots:
void onResultsFound( const QList<Tomahawk::result_ptr>& results );
void onResolvingFinished();
void updaterDestroyed();
private:
Playlist();
@ -274,8 +298,9 @@ private:
QList< plentry_ptr > m_entries;
QQueue<RevisionQueueItem> m_revisionQueue;
QQueue<RevisionQueueItem> m_updateQueue;
PlaylistUpdaterInterface* m_updater;
QWeakPointer<PlaylistUpdaterInterface> m_updater;
bool m_locallyChanged;
bool m_deleted;
@ -287,5 +312,6 @@ private:
}
Q_DECLARE_METATYPE( QSharedPointer< Tomahawk::Playlist > )
Q_DECLARE_METATYPE( QList< QSharedPointer< Tomahawk::PlaylistEntry > > )
#endif // PLAYLIST_H

View File

@ -23,6 +23,8 @@
#include <QTextOption>
#include "dllmacro.h"
#include "typedefs.h"
namespace Tomahawk {
class PixmapDelegateFader;

View File

@ -24,7 +24,7 @@
#include <QTextOption>
#include "dllmacro.h"
#include <signal.h>
#include "typedefs.h"
namespace Tomahawk {
class PixmapDelegateFader;

View File

@ -18,10 +18,19 @@
#include "PlaylistUpdaterInterface.h"
#include "tomahawksettings.h"
#include "XspfUpdater.h"
using namespace Tomahawk;
QMap< QString, PlaylistUpdaterFactory* > PlaylistUpdaterInterface::s_factories = QMap< QString, PlaylistUpdaterFactory* >();
void
PlaylistUpdaterInterface::registerUpdaterFactory( PlaylistUpdaterFactory* f )
{
s_factories[ f->type() ] = f;
}
PlaylistUpdaterInterface*
PlaylistUpdaterInterface::loadForPlaylist( const playlist_ptr& pl )
{
@ -32,19 +41,15 @@ PlaylistUpdaterInterface::loadForPlaylist( const playlist_ptr& pl )
// Ok, we have one we can try to load
const QString type = s->value( QString( "%1/type" ).arg( key ) ).toString();
PlaylistUpdaterInterface* updater = 0;
if ( type == "xspf" )
updater = new XspfUpdater( pl );
// You forgot to register your new updater type with the factory above. 00ps.
if ( !updater )
if ( !s_factories.contains( type ) )
{
Q_ASSERT( false );
// You forgot to register your new updater type with the factory....
return 0;
}
updater->setAutoUpdate( s->value( QString( "%1/autoupdate" ).arg( key ) ).toBool() );
updater->setInterval( s->value( QString( "%1/interval" ).arg( key ) ).toInt() );
updater->loadFromSettings( key );
updater = s_factories[ type ]->create( pl, key );
return updater;
}
@ -54,46 +59,26 @@ PlaylistUpdaterInterface::loadForPlaylist( const playlist_ptr& pl )
PlaylistUpdaterInterface::PlaylistUpdaterInterface( const playlist_ptr& pl )
: QObject( 0 )
, m_timer( new QTimer( this ) )
, m_autoUpdate( true )
, m_playlist( pl )
{
Q_ASSERT( !m_playlist.isNull() );
m_playlist->setUpdater( this );
connect( m_timer, SIGNAL( timeout() ), this, SLOT( updateNow() ) );
QTimer::singleShot( 0, this, SLOT( doSave() ) );
}
PlaylistUpdaterInterface::PlaylistUpdaterInterface( const playlist_ptr& pl, int interval, bool autoUpdate )
: QObject( 0 )
, m_timer( new QTimer( this ) )
, m_autoUpdate( autoUpdate )
, m_playlist( pl )
{
Q_ASSERT( !m_playlist.isNull() );
m_playlist->setUpdater( this );
m_timer->setInterval( interval );
connect( m_timer, SIGNAL( timeout() ), this, SLOT( updateNow() ) );
QTimer::singleShot( 0, this, SLOT( doSave() ) );
QTimer::singleShot( 0, this, SLOT( save() ) );
}
void
PlaylistUpdaterInterface::doSave()
PlaylistUpdaterInterface::save()
{
TomahawkSettings* s = TomahawkSettings::instance();
const QString key = QString( "playlistupdaters/%1" ).arg( m_playlist->guid() );
if ( !s->contains( QString( "%1/type" ).arg( key ) ) )
{
s->setValue( QString( "%1/type" ).arg( key ), type() );
s->setValue( QString( "%1/autoupdate" ).arg( key ), m_autoUpdate );
s->setValue( QString( "%1/interval" ).arg( key ), m_timer->interval() );
saveToSettings( key );
}
saveToSettings( key );
}
void
@ -106,36 +91,6 @@ PlaylistUpdaterInterface::remove()
const QString key = QString( "playlistupdaters/%1" ).arg( m_playlist->guid() );
removeFromSettings( key );
s->remove( QString( "%1/type" ).arg( key ) );
s->remove( QString( "%1/autoupdate" ).arg( key ) );
s->remove( QString( "%1/interval" ).arg( key ) );
deleteLater();
}
void
PlaylistUpdaterInterface::setAutoUpdate( bool autoUpdate )
{
m_autoUpdate = autoUpdate;
if ( m_autoUpdate )
m_timer->start();
else
m_timer->stop();
const QString key = QString( "playlistupdaters/%1/autoupdate" ).arg( m_playlist->guid() );
TomahawkSettings::instance()->setValue( key, m_autoUpdate );
// Update immediately as well
if ( m_autoUpdate )
QTimer::singleShot( 0, this, SLOT( updateNow() ) );
}
void
PlaylistUpdaterInterface::setInterval( int intervalMsecs )
{
const QString key = QString( "playlistupdaters/%1/interval" ).arg( m_playlist->guid() );
TomahawkSettings::instance()->setValue( key, intervalMsecs );
m_timer->setInterval( intervalMsecs );
}

View File

@ -22,33 +22,42 @@
#include "dllmacro.h"
#include "typedefs.h"
#include "playlist.h"
#include <QTimer>
#include <QMutex>
#ifndef ENABLE_HEADLESS
#include <QPixmap>
#endif
namespace Tomahawk
{
/**
* If a playlist needs periodic updating, implement a updater interface.
*
* Default is auto-updating.
* PlaylistUpdaters are attached to playlists. They usually manipulate the playlist in some way
* due to external input (spotify syncing) or timers (xspf updating)
*/
class PlaylistUpdaterFactory;
class DLLEXPORT PlaylistUpdaterInterface : public QObject
{
Q_OBJECT
public:
PlaylistUpdaterInterface( const playlist_ptr& pl );
PlaylistUpdaterInterface( const playlist_ptr& pl, int interval, bool autoUpdate );
explicit PlaylistUpdaterInterface( const playlist_ptr& pl );
virtual ~PlaylistUpdaterInterface(){}
// What type you are. If you add a new updater, add the creation code as well.
virtual QString type() const = 0;
bool autoUpdate() const { return m_autoUpdate; }
void setAutoUpdate( bool autoUpdate );
#ifndef ENABLE_HEADLESS
// Small widget to show in playlist header that configures the updater
virtual QWidget* configurationWidget() const = 0;
void setInterval( int intervalMsecs ) ;
int intervalMsecs() const { return m_timer->interval(); }
// Small overlay over playlist icon in the sidebar to indicate that it has this updater type
// Should be around 16x16 or something
virtual QPixmap typeIcon() const { return QPixmap(); }
#endif
void remove();
@ -58,21 +67,35 @@ public:
/// updater if one was saved
static PlaylistUpdaterInterface* loadForPlaylist( const playlist_ptr& pl );
static void registerUpdaterFactory( PlaylistUpdaterFactory* f );
signals:
void changed();
public slots:
virtual void updateNow() {}
private slots:
void doSave();
void save();
protected:
virtual void loadFromSettings( const QString& group ) = 0;
virtual void saveToSettings( const QString& group ) const = 0;
virtual void removeFromSettings( const QString& group ) const = 0;
private:
QTimer* m_timer;
bool m_autoUpdate;
playlist_ptr m_playlist;
static QMap< QString, PlaylistUpdaterFactory* > s_factories;
};
class DLLEXPORT PlaylistUpdaterFactory
{
public:
PlaylistUpdaterFactory() {}
virtual ~PlaylistUpdaterFactory() {}
virtual QString type() const = 0;
virtual PlaylistUpdaterInterface* create( const playlist_ptr&, const QString& settingsKey ) = 0;
};
}

View File

@ -26,32 +26,58 @@
#include <QTimer>
#ifndef ENABLE_HEADLESS
#include <QCheckBox>
#endif
using namespace Tomahawk;
XspfUpdater::XspfUpdater( const playlist_ptr& pl, const QString& xUrl )
: PlaylistUpdaterInterface( pl )
, m_url( xUrl )
PlaylistUpdaterInterface*
XspfUpdaterFactory::create( const playlist_ptr &pl, const QString& settingsKey )
{
const bool autoUpdate = TomahawkSettings::instance()->value( QString( "%1/autoupdate" ).arg( settingsKey ) ).toBool();
const int interval = TomahawkSettings::instance()->value( QString( "%1/interval" ).arg( settingsKey ) ).toInt();
const QString url = TomahawkSettings::instance()->value( QString( "%1/xspfurl" ).arg( settingsKey ) ).toString();
XspfUpdater* updater = new XspfUpdater( pl, interval, autoUpdate, url );
return updater;
}
XspfUpdater::XspfUpdater( const playlist_ptr& pl, int interval, bool autoUpdate, const QString& xspfUrl )
: PlaylistUpdaterInterface( pl, interval, autoUpdate )
: PlaylistUpdaterInterface( pl )
, m_timer( new QTimer( this ) )
, m_autoUpdate( autoUpdate )
, m_url( xspfUrl )
{
m_timer->setInterval( interval );
}
connect( m_timer, SIGNAL( timeout() ), this, SLOT( updateNow() ) );
#ifndef ENABLE_HEADLESS
m_toggleCheckbox = new QCheckBox( );
m_toggleCheckbox->setText( tr( "Automatically update" ) );
m_toggleCheckbox->setLayoutDirection( Qt::RightToLeft );
m_toggleCheckbox->setChecked( m_autoUpdate );
m_toggleCheckbox->hide();
XspfUpdater::XspfUpdater( const playlist_ptr& pl )
: PlaylistUpdaterInterface( pl )
{
connect( m_toggleCheckbox, SIGNAL( toggled( bool ) ), this, SLOT( setAutoUpdate( bool ) ) );
#endif
}
XspfUpdater::~XspfUpdater()
{}
QWidget*
XspfUpdater::configurationWidget() const
{
return m_toggleCheckbox;
}
void
XspfUpdater::updateNow()
{
@ -68,6 +94,7 @@ XspfUpdater::updateNow()
connect( l, SIGNAL( tracks( QList<Tomahawk::query_ptr> ) ), this, SLOT( playlistLoaded( QList<Tomahawk::query_ptr> ) ) );
}
void
XspfUpdater::playlistLoaded( const QList<Tomahawk::query_ptr>& newEntries )
{
@ -85,20 +112,51 @@ XspfUpdater::playlistLoaded( const QList<Tomahawk::query_ptr>& newEntries )
playlist()->createNewRevision( uuid(), playlist()->currentrevision(), el );
}
void
XspfUpdater::saveToSettings( const QString& group ) const
{
TomahawkSettings::instance()->setValue( QString( "%1/autoupdate" ).arg( group ), m_autoUpdate );
TomahawkSettings::instance()->setValue( QString( "%1/interval" ).arg( group ), m_timer->interval() );
TomahawkSettings::instance()->setValue( QString( "%1/xspfurl" ).arg( group ), m_url );
}
void
XspfUpdater::loadFromSettings( const QString& group )
{
m_url = TomahawkSettings::instance()->value( QString( "%1/xspfurl" ).arg( group ) ).toString();
}
void
XspfUpdater::removeFromSettings( const QString& group ) const
{
TomahawkSettings::instance()->remove( QString( "%1/autoupdate" ).arg( group ) );
TomahawkSettings::instance()->remove( QString( "%1/interval" ).arg( group ) );
TomahawkSettings::instance()->remove( QString( "%1/xspfurl" ).arg( group ) );
}
void
XspfUpdater::setAutoUpdate( bool autoUpdate )
{
m_autoUpdate = autoUpdate;
if ( m_autoUpdate )
m_timer->start();
else
m_timer->stop();
const QString key = QString( "playlistupdaters/%1/autoupdate" ).arg( playlist()->guid() );
TomahawkSettings::instance()->setValue( key, m_autoUpdate );
// Update immediately as well
if ( m_autoUpdate )
QTimer::singleShot( 0, this, SLOT( updateNow() ) );
}
void
XspfUpdater::setInterval( int intervalMsecs )
{
const QString key = QString( "playlistupdaters/%1/interval" ).arg( playlist()->guid() );
TomahawkSettings::instance()->setValue( key, intervalMsecs );
if ( !m_timer )
m_timer = new QTimer( this );
m_timer->setInterval( intervalMsecs );
}

View File

@ -20,37 +20,62 @@
#define XSPFUPDATER_H
#include "PlaylistUpdaterInterface.h"
#include "dllmacro.h"
class QTimer;
class QCheckBox;
namespace Tomahawk
{
class XspfUpdater : public PlaylistUpdaterInterface
class DLLEXPORT XspfUpdater : public PlaylistUpdaterInterface
{
Q_OBJECT
public:
XspfUpdater( const playlist_ptr& pl, const QString& xspfUrl );
XspfUpdater( const playlist_ptr& pl, int interval, bool autoUpdate, const QString& xspfUrl );
explicit XspfUpdater( const playlist_ptr& pl ); // used by factory
virtual ~XspfUpdater();
virtual QString type() const { return "xspf"; }
#ifndef ENABLE_HEADLESS
virtual QWidget* configurationWidget() const;
#endif
bool autoUpdate() const { return m_autoUpdate; }
void setInterval( int intervalMsecs ) ;
int intervalMsecs() const { return m_timer->interval(); }
public slots:
void updateNow();
void setAutoUpdate( bool autoUpdate );
protected:
void loadFromSettings( const QString& group );
void saveToSettings( const QString& group ) const;
virtual void removeFromSettings(const QString& group) const;
void removeFromSettings(const QString& group) const;
private slots:
void playlistLoaded( const QList<Tomahawk::query_ptr> & );
private:
QTimer* m_timer;
bool m_autoUpdate;
QString m_url;
#ifndef ENABLE_HEADLESS
QCheckBox* m_toggleCheckbox;
#endif
};
class DLLEXPORT XspfUpdaterFactory : public PlaylistUpdaterFactory
{
public:
XspfUpdaterFactory() {}
virtual QString type() const { return "xspf"; }
virtual PlaylistUpdaterInterface* create( const playlist_ptr& pl, const QString& settingsKey );
};
}

View File

@ -181,7 +181,7 @@ PlaylistItemDelegate::paintShort( QPainter* painter, const QStyleOptionViewItem&
painter->setPen( opt.palette.text().color() );
QRect ir = r.adjusted( 4, 0, -option.rect.width() + option.rect.height() - 8 + r.left(), 0 );
if ( useAvatars )
{
if ( !source.isNull() )

View File

@ -41,6 +41,8 @@ PlaylistModel::PlaylistModel( QObject* parent )
: TrackModel( parent )
, m_isTemporary( false )
, m_changesOngoing( false )
, m_isLoading( false )
, m_savedInsertPos( -1 )
{
m_dropStorage.parent = QPersistentModelIndex();
m_dropStorage.row = -10;
@ -64,6 +66,8 @@ PlaylistModel::loadPlaylist( const Tomahawk::playlist_ptr& playlist, bool loadEn
disconnect( m_playlist.data(), SIGNAL( changed() ), this, SIGNAL( playlistChanged() ) );
}
m_isLoading = true;
if ( loadEntries )
clear();
@ -79,11 +83,22 @@ PlaylistModel::loadPlaylist( const Tomahawk::playlist_ptr& playlist, bool loadEn
.arg( TomahawkUtils::ageToString( QDateTime::fromTime_t( playlist->createdOn() ), true ) ) );
m_isTemporary = false;
if ( !loadEntries )
{
m_isLoading = false;
return;
}
QList<plentry_ptr> entries = playlist->entries();
qDebug() << "playlist loading entries:";
foreach( const plentry_ptr& p, entries )
qDebug() << p->guid() << p->query()->track() << p->query()->artist();
append( entries );
m_isLoading = false;
}
@ -178,7 +193,11 @@ PlaylistModel::insert( const QList< Tomahawk::query_ptr >& queries, int row )
entry->setDuration( 0 );
entry->setLastmodified( 0 );
entry->setAnnotation( "" ); // FIXME
QString annotation = "";
if ( !query->property( "annotation" ).toString().isEmpty() )
annotation = query->property( "annotation" ).toString();
entry->setAnnotation( annotation );
entry->setQuery( query );
entry->setGuid( uuid() );
@ -203,6 +222,12 @@ PlaylistModel::insert( const QList< Tomahawk::plentry_ptr >& entries, int row )
crows.first = c;
crows.second = c + entries.count() - 1;
if ( !m_isLoading )
{
m_savedInsertPos = row;
m_savedInsertTracks = entries;
}
emit beginInsertRows( QModelIndex(), crows.first, crows.second );
QList< Tomahawk::query_ptr > queries;
@ -402,6 +427,46 @@ PlaylistModel::endPlaylistChanges()
{
m_playlist->createNewRevision( newrev, m_playlist->currentrevision(), l );
}
if ( m_savedInsertPos >= 0 && !m_savedInsertTracks.isEmpty() &&
!m_savedRemoveTracks.isEmpty() )
{
// If we have *both* an insert and remove, then it's a move action
// However, since we got the insert before the remove (Qt...), the index we have as the saved
// insert position is no longer valid. Find the proper one by finding the location of the first inserted
// track
for ( int i = 0; i < rowCount( QModelIndex() ); i++ )
{
const QModelIndex idx = index( i, 0, QModelIndex() );
if ( !idx.isValid() )
continue;
const TrackModelItem* item = itemFromIndex( idx );
if ( !item || item->entry().isNull() )
continue;
// qDebug() << "Checking for equality:" << (item->entry() == m_savedInsertTracks.first()) << m_savedInsertTracks.first()->query()->track() << m_savedInsertTracks.first()->query()->artist();
if ( item->entry() == m_savedInsertTracks.first() )
{
// Found our index
emit m_playlist->tracksMoved( m_savedInsertTracks, i );
break;
}
}
m_savedInsertPos = -1;
m_savedInsertTracks.clear();
m_savedRemoveTracks.clear();
}
else if ( m_savedInsertPos >= 0 )
{
emit m_playlist->tracksInserted( m_savedInsertTracks, m_savedInsertPos );
m_savedInsertPos = -1;
m_savedInsertTracks.clear();
}
else if ( !m_savedRemoveTracks.isEmpty() )
{
emit m_playlist->tracksRemoved( m_savedRemoveTracks );
m_savedRemoveTracks.clear();
}
}
@ -447,6 +512,9 @@ PlaylistModel::remove( const QModelIndex& index, bool moreToCome )
if ( !m_changesOngoing )
beginPlaylistChanges();
if ( item && !m_isLoading )
m_savedRemoveTracks << item->query();
TrackModel::remove( index, moreToCome );
if ( !moreToCome )

View File

@ -97,9 +97,14 @@ private:
Tomahawk::playlist_ptr m_playlist;
bool m_isTemporary;
bool m_changesOngoing;
bool m_isLoading;
QList< Tomahawk::Query* > m_waitingForResolved;
QStringList m_waitForRevision;
int m_savedInsertPos;
QList< Tomahawk::plentry_ptr > m_savedInsertTracks;
QList< Tomahawk::query_ptr > m_savedRemoveTracks;
DropStorageData m_dropStorage;
};

View File

@ -119,23 +119,13 @@ PlaylistView::canAutoUpdate() const
}
bool
PlaylistView::autoUpdate() const
PlaylistUpdaterInterface*
PlaylistView::autoUpdateInterface() const
{
if ( canAutoUpdate() )
return m_model->playlist()->updater()->autoUpdate();
if ( !m_model->playlist().isNull() && m_model->playlist()->updater() )
return m_model->playlist()->updater();
return false;
}
void
PlaylistView::setAutoUpdate( bool autoUpdate )
{
if ( !canAutoUpdate() )
return;
m_model->playlist()->updater()->setAutoUpdate( autoUpdate );
return 0;
}

View File

@ -44,8 +44,7 @@ public:
virtual bool showFilter() const { return true; }
virtual bool canAutoUpdate() const;
virtual void setAutoUpdate( bool autoUpdate );
virtual bool autoUpdate() const;
virtual Tomahawk::PlaylistUpdaterInterface* autoUpdateInterface() const;
virtual QString title() const { return playlistModel()->title(); }
virtual QString description() const { return m_model->description(); }

View File

@ -22,6 +22,7 @@
#include <QStyledItemDelegate>
#include "dllmacro.h"
#include "typedefs.h"
namespace Tomahawk {
class PixmapDelegateFader;

View File

@ -221,4 +221,6 @@ private:
}; //ns
Q_DECLARE_METATYPE(Tomahawk::query_ptr);
#endif // QUERY_H

View File

@ -1,6 +1,7 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,7 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -65,8 +65,11 @@ ScriptResolver::~ScriptResolver()
disconnect( &m_proc, SIGNAL( finished( int, QProcess::ExitStatus ) ), this, SLOT( cmdExited( int, QProcess::ExitStatus ) ) );
m_deleting = true;
m_proc.kill();
m_proc.waitForFinished(); // might call handleMsg
QVariantMap msg;
msg[ "_msgtype" ] = "quit";
sendMessage( msg );
m_proc.waitForFinished( 1000 ); // might call handleMsg
Tomahawk::Pipeline::instance()->removeResolver( this );
@ -146,6 +149,13 @@ ScriptResolver::running() const
return !m_stopped;
}
void
ScriptResolver::sendMessage( const QVariantMap& map )
{
QByteArray data = m_serializer.serialize( map );
sendMsg( data );
}
void
ScriptResolver::readStderr()
@ -272,24 +282,10 @@ ScriptResolver::handleMsg( const QByteArray& msg )
Tomahawk::Pipeline::instance()->reportResults( qid, results );
}
else if ( msgtype == "playlist" )
else
{
QList< Tomahawk::query_ptr > tracks;
const QString qid = m.value( "qid" ).toString();
const QString title = m.value( "identifier" ).toString();
const QVariantList reslist = m.value( "playlist" ).toList();
if( !reslist.isEmpty() )
{
foreach( const QVariant& rv, reslist )
{
QVariantMap m = rv.toMap();
qDebug() << "Found playlist result:" << m;
Tomahawk::query_ptr q = Tomahawk::Query::get( m.value( "artist" ).toString() , m.value( "track" ).toString() , QString(), uuid(), false );
tracks << q;
}
}
// Unknown message, give up for custom implementations
emit customMessage( msgtype, m );
}
}

View File

@ -54,8 +54,11 @@ public:
virtual bool running() const;
void sendMessage( const QVariantMap& map );
signals:
void terminated();
void customMessage( const QString& msgType, const QVariantMap& msg );
public slots:
virtual void stop();

View File

@ -1121,3 +1121,20 @@ TomahawkSettings::updateIndex()
DatabaseCommand* cmd = new DatabaseCommand_UpdateSearchIndex();
Database::instance()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
}
QString
TomahawkSettings::importXspfPath() const
{
if ( contains( "importXspfPath" ) )
return value( "importXspfPath" ).toString();
else
return QDir::homePath();
}
void
TomahawkSettings::setImportXspfPath( const QString& path )
{
setValue( "importXspfPath", path );
}

View File

@ -198,6 +198,9 @@ public:
PrivateListeningMode privateListeningMode() const;
void setPrivateListeningMode( PrivateListeningMode mode );
void setImportXspfPath( const QString& path );
QString importXspfPath() const;
signals:
void changed();
void recentlyPlayedPlaylistAdded( const Tomahawk::playlist_ptr& playlist );

View File

@ -23,6 +23,7 @@
#include <QSharedPointer>
#include <QUuid>
#include <QPair>
#include <QPersistentModelIndex>
#include <boost/function.hpp>
//template <typename T> class QSharedPointer;
@ -90,4 +91,6 @@ inline static QString uuid()
return q;
}
Q_DECLARE_METATYPE( QPersistentModelIndex )
#endif // TYPEDEFS_H

View File

@ -0,0 +1,204 @@
/****************************************************************************************
* Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org> *
* Copyright (c) 2009 Ian Monroe <ian@monroe.nu> *
* Copyright (c) 2009 Max Howell <max@last.fm> *
* *
* 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/>. *
****************************************************************************************/
#ifndef SMART_POINTER_LIST_H
#define SMART_POINTER_LIST_H
#include <QList> //baseclass
#include <QObject> //baseclass
#include "dllmacro.h"
class DLLEXPORT SmartPointerListDaddy : public QObject
{
Q_OBJECT
QList<QObject*>& m_list;
public:
SmartPointerListDaddy( QList<QObject*>* list ) : m_list( *list )
{}
private slots:
void onDestroyed()
{
m_list.removeAll( sender() );
}
};
/** QList has no virtual functions, so we inherit privately and define the
* interface exactly to ensure users can't write code that breaks the
* class's internal behaviour.
*
* I deliberately didn't define clear. I worried people would assume it
* deleted the pointers. Or assume it didn't. I didn't expose a few other
* functions for that reason.
*
* non-const iterator functions are not exposed as they access the QList
* baseclass, and then the Daddy wouldn't be watching newly inserted items.
*
* --mxcl
* Exposed clear. This class doesn't have a QPtrList autodelete functionality
* ever, so if people think that, they're really confused! -- Ian Monroe
*
*/
template <class T> class SmartPointerList : private QList<T*>
{
class SmartPointerListDaddy* m_daddy;
public:
SmartPointerList() : m_daddy( new SmartPointerListDaddy( (QList<QObject*>*)this ) )
{}
~SmartPointerList()
{
delete m_daddy;
}
SmartPointerList( const SmartPointerList<T>& that )
: QList<T*>()
, m_daddy( new SmartPointerListDaddy( (QList<QObject*>*)this ) )
{
QListIterator<T*> i( that );
while (i.hasNext())
append( i.next() );
}
SmartPointerList& operator=( const SmartPointerList<T>& that )
{
QListIterator<T*> i( *this);
while (i.hasNext())
QObject::disconnect( m_daddy, 0, i.next(), 0 );
QList<T*>::operator=( that );
if (this != &that) {
QListIterator<T*> i( that );
while (i.hasNext())
m_daddy->connect( i.next(), SIGNAL(destroyed()), SLOT(onDestroyed()) );
}
return *this;
}
// keep same function names as Qt
void append( T* o )
{
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
QList<T*>::append( o );
}
void prepend( T* o )
{
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
QList<T*>::prepend( o );
}
SmartPointerList& operator+=( T* o )
{
append( o );
return *this;
}
SmartPointerList& operator<<( T* o )
{
return operator+=( o );
}
SmartPointerList operator+( const SmartPointerList that )
{
SmartPointerList<T> copy = *this;
QListIterator<T*> i( that );
while (i.hasNext())
copy.append( i.next() );
return copy;
}
SmartPointerList& operator+=( const SmartPointerList that )
{
QListIterator<T*> i( that );
while (i.hasNext())
append( i.next() );
return *this;
}
void push_back( T* o )
{
append( o );
}
void push_front( T* o )
{
prepend( o );
}
void replace( int i, T* o )
{
QList<T*>::replace( i, o );
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
}
/** this is a "safe" class. We always bounds check */
inline T* operator[]( int index ) const { return QList<T*>::value( index ); }
inline T* at( int index ) const { return QList<T*>::value( index ); }
// make public safe functions again
using QList<T*>::back;
using QList<T*>::constBegin;
using QList<T*>::constEnd;
using typename QList<T*>::const_iterator;
using QList<T*>::contains;
using QList<T*>::count;
using QList<T*>::empty;
using QList<T*>::erase;
using QList<T*>::first;
using QList<T*>::front;
using QList<T*>::indexOf;
using QList<T*>::insert;
using QList<T*>::isEmpty;
using QList<T*>::last;
using QList<T*>::lastIndexOf;
using QList<T*>::mid;
using QList<T*>::move;
using QList<T*>::pop_back;
using QList<T*>::pop_front;
using QList<T*>::size;
using QList<T*>::swap;
using QList<T*>::value;
using QList<T*>::operator!=;
using QList<T*>::operator==;
// can't use using directive here since we only want the const versions
typename QList<T*>::const_iterator begin() const { return QList<T*>::constBegin(); }
typename QList<T*>::const_iterator end() const { return QList<T*>::constEnd(); }
// it can lead to poor performance situations if we don't disconnect
// but I think it's not worth making this class more complicated for such
// an edge case
using QList<T*>::clear;
using QList<T*>::removeAll;
using QList<T*>::removeAt;
using QList<T*>::removeFirst;
using QList<T*>::removeLast;
using QList<T*>::removeOne;
using QList<T*>::takeAt;
using QList<T*>::takeFirst;
using QList<T*>::takeLast;
};
#endif //HEADER_GUARD

View File

@ -54,7 +54,17 @@ Closure::Closure(QObject* sender,
Closure::~Closure() {
}
void Closure::forceInvoke()
{
Invoked();
}
void Closure::Connect(QObject* sender, const char* signal) {
if (!sender)
return;
bool success = connect(sender, signal, SLOT(Invoked()));
Q_ASSERT(success);
success = connect(sender, SIGNAL(destroyed()), SLOT(Cleanup()));

View File

@ -18,6 +18,8 @@
#ifndef CLOSURE_H
#define CLOSURE_H
#include "dllmacro.h"
#include <tr1/functional>
#include <QMetaMethod>
@ -29,7 +31,7 @@
namespace _detail {
class ClosureArgumentWrapper {
class DLLEXPORT ClosureArgumentWrapper {
public:
virtual ~ClosureArgumentWrapper() {}
@ -42,14 +44,14 @@ class ClosureArgument : public ClosureArgumentWrapper {
explicit ClosureArgument(const T& data) : data_(data) {}
virtual QGenericArgument arg() const {
return Q_ARG(T, data_);
return QArgument<T>(QMetaType::typeName(qMetaTypeId<T>()), data_);
}
private:
T data_;
};
class Closure : public QObject, boost::noncopyable {
class DLLEXPORT Closure : public QObject, boost::noncopyable {
Q_OBJECT
public:
@ -67,6 +69,15 @@ class Closure : public QObject, boost::noncopyable {
virtual ~Closure();
/**
* If you don't this Closure to act on a signal, but just act like
* a closure in that it saves some args and delivers them on demand later
*
* Only call this is you passed a null QObject* as a sender! Otherwise you
* might delete your object twice :)
*/
void forceInvoke();
private slots:
void Invoked();
void Cleanup();
@ -84,7 +95,7 @@ class Closure : public QObject, boost::noncopyable {
boost::scoped_ptr<const ClosureArgumentWrapper> val3_;
};
class SharedPointerWrapper {
class DLLEXPORT SharedPointerWrapper {
public:
virtual ~SharedPointerWrapper() {}
virtual QObject* data() const = 0;

View File

@ -267,7 +267,6 @@ XSPFLoader::gotBody()
m_entries );
// 10 minute default---for now, no way to change it
connect( m_playlist.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ), this, SLOT( playlistCreated() ) );
new Tomahawk::XspfUpdater( m_playlist, 600000, m_autoUpdate, m_url.toString() );
emit ok( m_playlist );
}

View File

@ -117,7 +117,6 @@ ViewManager::ViewManager( QObject* parent )
connect( &m_filterTimer, SIGNAL( timeout() ), SLOT( applyFilter() ) );
connect( m_infobar, SIGNAL( filterTextChanged( QString ) ), SLOT( setFilter( QString ) ) );
connect( m_infobar, SIGNAL( autoUpdateChanged( bool ) ), SLOT( autoUpdateChanged( bool ) ) );
connect( this, SIGNAL( tomahawkLoaded() ), m_whatsHotWidget, SLOT( fetchData() ) );
connect( this, SIGNAL( tomahawkLoaded() ), m_newReleasesWidget, SLOT( fetchData() ) );
@ -583,13 +582,6 @@ ViewManager::applyFilter()
}
void
ViewManager::autoUpdateChanged( bool toggled )
{
currentPage()->setAutoUpdate( toggled );
}
void
ViewManager::setPage( ViewPage* page, bool trackHistory )
{
@ -745,8 +737,6 @@ ViewManager::updateView()
emit modesAvailable( currentPage()->showModes() );
emit filterAvailable( currentPage()->showFilter() );
emit autoUpdateAvailable( currentPage()->canAutoUpdate() );
/* if ( !currentPage()->showStatsBar() && !currentPage()->showModes() && !currentPage()->showFilter() )
m_topbar->setVisible( false );
else
@ -754,6 +744,9 @@ ViewManager::updateView()
m_infobar->setVisible( currentPage()->showInfoBar() );
m_infobar->setCaption( currentPage()->title() );
m_infobar->setAutoUpdateInterface( currentPage()->autoUpdateInterface() );
switch( currentPage()->descriptionType() )
{
case ViewPage::TextType:

View File

@ -124,7 +124,6 @@ signals:
void statsAvailable( bool b );
void modesAvailable( bool b );
void filterAvailable( bool b );
void autoUpdateAvailable( bool b );
void modeChanged( Tomahawk::PlaylistInterface::ViewMode mode );
void playClicked();
@ -180,8 +179,6 @@ private slots:
void setFilter( const QString& filter );
void applyFilter();
void autoUpdateChanged( bool );
void onWidgetDestroyed( QWidget* widget );
private:

View File

@ -31,6 +31,8 @@
namespace Tomahawk
{
class PlaylistUpdaterInterface;
class DLLEXPORT ViewPage
{
public:
@ -68,8 +70,7 @@ public:
virtual bool isBeingPlayed() const { return false; }
virtual bool canAutoUpdate() const { return false; }
virtual void setAutoUpdate( bool ) {}
virtual bool autoUpdate() const { return false; }
virtual PlaylistUpdaterInterface* autoUpdateInterface() const { return 0; }
/** subclasses implementing ViewPage can emit the following signals:
* nameChanged( const QString& )

View File

@ -59,7 +59,6 @@ ChartDataLoader::go()
foreach ( const Tomahawk::InfoSystem::InfoStringHash& album, m_data )
{
tDebug( LOGVERBOSE) << Q_FUNC_INFO << "Getting album" << album[ "album" ] << "By" << album[ "artist" ];
artist_ptr artistPtr = Artist::get( album[ "artist" ], false );
album_ptr albumPtr = Album::get( artistPtr, album[ "album" ], false );
album_ptrs << albumPtr;

View File

@ -125,7 +125,7 @@ main( int argc, char *argv[] )
#endif
#ifndef ENABLE_HEADLESS
#ifndef ENABLE_HEADLESSs
#ifdef WITH_BREAKPAD
new BreakPad( QDir::tempPath(), TomahawkSettings::instance()->crashReporterEnabled() );
#endif

View File

@ -20,6 +20,7 @@
#include "playlistitems.h"
#include <QMimeData>
#include <QPainter>
#include "query.h"
#include "viewmanager.h"
@ -44,12 +45,15 @@ PlaylistItem::PlaylistItem( SourcesModel* mdl, SourceTreeItem* parent, const pla
connect( pl.data(), SIGNAL( revisionLoaded( Tomahawk::PlaylistRevision ) ),
SLOT( onPlaylistLoaded( Tomahawk::PlaylistRevision ) ), Qt::QueuedConnection );
connect( pl.data(), SIGNAL( changed() ),
SIGNAL( updated() ), Qt::QueuedConnection );
SLOT( onUpdated() ), Qt::QueuedConnection );
m_icon = QIcon( RESPATH "images/playlist-icon.png" );
if( ViewManager::instance()->pageForPlaylist( pl ) )
model()->linkSourceItemToPage( this, ViewManager::instance()->pageForPlaylist( pl ) );
if ( m_playlist->updater() && !m_playlist->updater()->typeIcon().isNull() )
createOverlay();
}
@ -244,10 +248,54 @@ PlaylistItem::parsedDroppedTracks( const QList< query_ptr >& tracks )
}
void
PlaylistItem::onUpdated()
{
if ( m_playlist->updater() && !m_playlist->updater()->typeIcon().isNull() &&
m_overlaidIcon.isNull() ) // create overlay
{
createOverlay();
}
else if ( !m_playlist->updater() || m_playlist->updater()->typeIcon().isNull() &&
!m_overlaidIcon.isNull() )
{
// No longer an updater with an icon
m_overlaidIcon = QIcon();
}
emit updated();
}
void
PlaylistItem::createOverlay()
{
Q_ASSERT( !m_playlist.isNull() );
Q_ASSERT( m_playlist->updater() );
Q_ASSERT( !m_playlist->updater()->typeIcon().isNull() );
m_overlaidIcon = QIcon();
QPixmap base = m_icon.pixmap( 48, 48 );
const QPixmap overlay = m_playlist->updater()->typeIcon();
QPainter p( &base );
const int w = base.width() / 2;
const QRect overlayRect( base.rect().right() - w, base.rect().height() - w, w, w );
p.drawPixmap( overlayRect, overlay );
p.end();
m_overlaidIcon.addPixmap( base );
}
QIcon
PlaylistItem::icon() const
{
return m_icon;
if ( !m_overlaidIcon.isNull() )
return m_overlaidIcon;
else
return m_icon;
}

View File

@ -54,10 +54,13 @@ private slots:
void onPlaylistChanged();
void parsedDroppedTracks( const QList<Tomahawk::query_ptr>& tracks );
void onUpdated();
private:
void createOverlay();
bool m_loaded;
Tomahawk::playlist_ptr m_playlist;
QIcon m_icon;
QIcon m_icon, m_overlaidIcon;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::DropTypes)

View File

@ -143,11 +143,12 @@ SourceTreeView::setupMenus()
bool readonly = true;
SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt();
if ( type == SourcesModel::StaticPlaylist || type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station )
{
const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex );
const playlist_ptr playlist = item->playlist();
PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex );
playlist_ptr playlist = item->playlist();
if ( !playlist.isNull() )
{
readonly = !playlist->author()->isLocal();
@ -190,7 +191,7 @@ SourceTreeView::setupMenus()
QString addToText = QString( "Add to my %1" );
if ( type == SourcesModel::StaticPlaylist )
addToText = addToText.arg( "Playlists" );
addToText = addToText.arg( "playlists" );
if ( type == SourcesModel::AutomaticPlaylist )
addToText = addToText.arg( "Automatic Playlists" );
else if ( type == SourcesModel::Station )
@ -203,6 +204,28 @@ SourceTreeView::setupMenus()
renamePlaylistAction->setEnabled( !readonly );
addToLocalAction->setEnabled( readonly );
// Handle any custom actions registered for playlists
if ( type == SourcesModel::StaticPlaylist && !readonly &&
!ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ).isEmpty() )
{
m_playlistMenu.addSeparator();
const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex );
const playlist_ptr playlist = item->playlist();
foreach ( QAction* action, ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ) )
{
if ( QObject* notifier = ActionCollection::instance()->actionNotifier( action ) )
{
QMetaObject::invokeMethod( notifier, "aboutToShow", Qt::DirectConnection, Q_ARG( QAction*, action ), Q_ARG( Tomahawk::playlist_ptr, playlist ) );
}
action->setProperty( "payload", QVariant::fromValue< playlist_ptr >( playlist ) );
m_playlistMenu.addAction( action );
}
}
if ( type == SourcesModel::StaticPlaylist )
copyPlaylistAction->setText( tr( "&Export Playlist" ) );

View File

@ -44,6 +44,7 @@
#include "playlist/dynamic/GeneratorFactory.h"
#include "playlist/dynamic/echonest/EchonestGenerator.h"
#include "playlist/dynamic/database/DatabaseGenerator.h"
#include "playlist/XspfUpdater.h"
#include "network/servent.h"
#include "web/api_v1.h"
#include "sourcelist.h"
@ -62,6 +63,9 @@
#include "utils/jspfloader.h"
#include "utils/logger.h"
#include "utils/tomahawkutilsgui.h"
#include "accounts/lastfm/LastFmAccount.h"
#include "accounts/spotify/SpotifyAccount.h"
#include "accounts/spotify/SpotifyPlaylistUpdater.h"
#include "utils/tomahawkcache.h"
#include "config.h"
@ -283,6 +287,9 @@ TomahawkApp::init()
// Set up echonest catalog synchronizer
Tomahawk::EchonestCatalogSynchronizer::instance();
PlaylistUpdaterInterface::registerUpdaterFactory( new XspfUpdaterFactory );
PlaylistUpdaterInterface::registerUpdaterFactory( new SpotifyUpdaterFactory );
#ifndef ENABLE_HEADLESS
// Make sure to init GAM in the gui thread
GlobalActionManager::instance();
@ -660,13 +667,15 @@ TomahawkApp::instanceStarted( KDSingleApplicationGuard::Instance instance )
{
tDebug( LOGINFO ) << "Instance started!" << instance.pid << instance.arguments;
activate();
if ( instance.arguments.size() < 2 )
return;
QString arg1 = instance.arguments[ 1 ];
if ( loadUrl( arg1 ) )
{
activate();
return;
}
if ( instance.arguments.contains( "--next" ) )
AudioEngine::instance()->next();

View File

@ -147,8 +147,6 @@ private:
QWeakPointer< QxtHttpSessionManager > m_session;
};
Q_DECLARE_METATYPE( QPersistentModelIndex )
Q_DECLARE_METATYPE( PairList )
#endif // TOMAHAWKAPP_H

View File

@ -2,6 +2,7 @@
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Jeff Mitchell <jeff@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

View File

@ -1,6 +1,7 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2011, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by