1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-08-23 06:02:53 +02:00

Initial Tomahawk import.

This commit is contained in:
Christian Muehlhaeuser
2010-10-17 05:32:01 +02:00
commit 1f592fbbd9
413 changed files with 48521 additions and 0 deletions

10
src/CMakeLists.linux.txt Normal file
View File

@@ -0,0 +1,10 @@
IF( "${gui}" STREQUAL "no" )
ELSE()
SET( OS_SPECIFIC_LINK_LIBRARIES
${OS_SPECIFIC_LINK_LIBRARIES}
alsaplayback
gnutls
)
ENDIF()
#include( "CPack.txt" )

21
src/CMakeLists.osx.txt Normal file
View File

@@ -0,0 +1,21 @@
IF( "${gui}" STREQUAL "no" )
ELSE()
SET( tomahawkSourcesGui ${tomahawkSourcesGui}
audio/rtaudiooutput.cpp
)
SET( tomahawkHeadersGui ${tomahawkHeadersGui}
audio/rtaudiooutput.h
)
FIND_LIBRARY( COREAUDIO_LIBRARY CoreAudio )
FIND_LIBRARY( COREFOUNDATION_LIBRARY CoreFoundation )
MARK_AS_ADVANCED( COREAUDIO_LIBRARY COREFOUNDATION_LIBRARY )
SET( OS_SPECIFIC_LINK_LIBRARIES
${OS_SPECIFIC_LINK_LIBRARIES}
${COREAUDIO_LIBRARY}
${COREFOUNDATION_LIBRARY}
rtaudio
)
ENDIF()

298
src/CMakeLists.txt Normal file
View File

@@ -0,0 +1,298 @@
PROJECT( tomahawk )
CMAKE_MINIMUM_REQUIRED( VERSION 2.8 )
IF( "${gui}" STREQUAL "no" )
SET( QT_DONT_USE_QTGUI TRUE )
ENDIF()
SET( QT_USE_QTSQL TRUE )
SET( QT_USE_QTNETWORK TRUE )
SET( QT_USE_QTXML TRUE )
INCLUDE( ${QT_USE_FILE} )
SET( CMAKE_BUILD_TYPE "debugfull" )
SET( CMAKE_VERBOSE_MAKEFILE ON )
SET( CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" )
SET( CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" )
SET( CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" )
SET( TOMAHAWK_INC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../include/" )
# build plugins
# use glob, but hardcoded list for now:
#FILE( GLOB plugindirs "src/plugins/*" )
#FOREACH( moddir ${plugindirs} )
# MESSAGE( status "Building plugin: ${moddir}" )
# ADD_SUBDIRECTORY( ${moddir} )
#ENDFOREACH( moddir )
SET( tomahawkSources ${tomahawkSources}
pipeline.cpp
playlist.cpp
pluginapi.cpp
query.cpp
result.cpp
source.cpp
sourcelist.cpp
utils/tomahawkutils.cpp
jabber/jabber_p.cpp
bufferiodevice.cpp
connection.cpp
msgprocessor.cpp
controlconnection.cpp
collection.cpp
filetransferconnection.cpp
dbsyncconnection.cpp
musicscanner.cpp
remotecollection.cpp
servent.cpp
scriptresolver.cpp
database/fuzzyindex.cpp
database/database.cpp
database/databaseworker.cpp
database/databaseimpl.cpp
database/databaseresolver.cpp
database/databasecommand.cpp
database/databasecommandloggable.cpp
database/databasecommand_resolve.cpp
database/databasecommand_alltracks.cpp
database/databasecommand_addfiles.cpp
database/databasecommand_dirmtimes.cpp
database/databasecommand_loadfile.cpp
database/databasecommand_addsource.cpp
database/databasecommand_sourceoffline.cpp
database/databasecommand_collectionstats.cpp
database/databasecommand_loadplaylistentries.cpp
database/databasecommand_modifyplaylist.cpp
database/databasecommand_setplaylistrevision.cpp
database/databasecommand_loadallplaylists.cpp
database/databasecommand_createplaylist.cpp
database/databasecommand_deleteplaylist.cpp
database/databasecommand_loadops.cpp
database/databasecommand_updatesearchindex.cpp
database/databasecollection.cpp
web/api_v1.cpp
widgetdragfilter.cpp
tomahawksettings.cpp
tomahawkapp.cpp
main.cpp
)
SET( tomahawkSourcesGui ${tomahawkSourcesGui}
xspfloader.cpp
# utils/modeltest.cpp
utils/animatedrowremover.cpp
utils/imagebutton.cpp
utils/progresstreeview.cpp
audio/madtranscode.cpp
audio/audioengine.cpp
playlist/playlistproxymodel.cpp
playlist/playlistmodel.cpp
playlist/playlistmodelworker.cpp
playlist/playlistitem.cpp
playlist/playlistitemdelegate.cpp
playlist/playlistview.cpp
sourcetree/sourcesmodel.cpp
sourcetree/sourcetreeitem.cpp
sourcetree/sourcetreeitemwidget.cpp
sourcetree/sourcetreeview.cpp
topbar/topbar.cpp
topbar/clearbutton.cpp
topbar/searchlineedit.cpp
topbar/lineedit.cpp
topbar/searchbutton.cpp
tomahawkwindow.cpp
audiocontrols.cpp
settingsdialog.cpp
proxystyle.cpp
)
SET( tomahawkHeaders ${tomahawkHeaders}
"${TOMAHAWK_INC_DIR}/tomahawk/tomahawkapp.h"
"${TOMAHAWK_INC_DIR}/tomahawk/collection.h"
"${TOMAHAWK_INC_DIR}/tomahawk/pipeline.h"
"${TOMAHAWK_INC_DIR}/tomahawk/pluginapi.h"
"${TOMAHAWK_INC_DIR}/tomahawk/query.h"
"${TOMAHAWK_INC_DIR}/tomahawk/resolver.h"
"${TOMAHAWK_INC_DIR}/tomahawk/result.h"
"${TOMAHAWK_INC_DIR}/tomahawk/source.h"
"${TOMAHAWK_INC_DIR}/tomahawk/sourcelist.h"
"${TOMAHAWK_INC_DIR}/tomahawk/artist.h"
"${TOMAHAWK_INC_DIR}/tomahawk/album.h"
"${TOMAHAWK_INC_DIR}/tomahawk/track.h"
"${TOMAHAWK_INC_DIR}/tomahawk/playlist.h"
"${TOMAHAWK_INC_DIR}/tomahawk/functimeout.h"
# "${TOMAHAWK_INC_DIR}/tomahawk/tomahawkplugin.h"
database/fuzzyindex.h
database/database.h
database/databaseworker.h
database/databaseimpl.h
database/databaseresolver.h
database/databasecommand.h
database/databasecommandloggable.h
database/databasecommand_resolve.h
database/databasecommand_alltracks.h
database/databasecommand_addfiles.h
database/databasecommand_dirmtimes.h
database/databasecommand_loadfile.h
database/databasecommand_addsource.h
database/databasecommand_sourceoffline.h
database/databasecommand_collectionstats.h
database/databasecommand_loadplaylistentries.h
database/databasecommand_modifyplaylist.h
database/databasecommand_setplaylistrevision.h
database/databasecommand_loadallplaylists.h
database/databasecommand_createplaylist.h
database/databasecommand_deleteplaylist.h
database/databasecommand_loadops.h
database/databasecommand_updatesearchindex.h
database/databasecollection.h
jabber/jabber.h
jabber/jabber_p.h
bufferiodevice.h
connection.h
msgprocessor.h
controlconnection.h
filetransferconnection.h
dbsyncconnection.h
musicscanner.h
tomahawkzeroconf.h
remotecollection.h
servent.h
scriptresolver.h
tomahawksettings.h
widgetdragfilter.h
web/api_v1.h
)
SET( tomahawkHeadersGui ${tomahawkHeadersGui}
xspfloader.h
# utils/modeltest.h
utils/animatedcounterlabel.h
utils/animatedrowremover.h
utils/imagebutton.h
utils/progresstreeview.h
audio/transcodeinterface.h
audio/madtranscode.h
audio/audioengine.h
playlist/playlistproxymodel.h
playlist/playlistmodel.h
playlist/playlistmodelworker.h
playlist/playlistitem.h
playlist/playlistitemdelegate.h
playlist/playlistview.h
sourcetree/sourcesmodel.h
sourcetree/sourcetreeitem.h
sourcetree/sourcetreeitemwidget.h
sourcetree/sourcetreeview.h
topbar/topbar.h
topbar/clearbutton.h
topbar/searchlineedit.h
topbar/lineedit.h
topbar/lineedit_p.h
topbar/searchbutton.h
tomahawkwindow.h
audiocontrols.h
settingsdialog.h
)
SET( tomahawkUI ${tomahawkUI}
tomahawkwindow.ui
settingsdialog.ui
audiocontrols.ui
sourcetree/sourcetreeitemwidget.ui
topbar/topbar.ui
)
INCLUDE_DIRECTORIES(
.
${TOMAHAWK_INC_DIR}
${CMAKE_CURRENT_BINARY_DIR}
audio
database
playlist
sourcetree
topbar
utils
../rtaudio
../alsa-playback
../libportfwd/include
../qxtweb-standalone/qxtweb/
/usr/include/taglib
/usr/local/include/taglib
/usr/local/include
)
SET( OS_SPECIFIC_LINK_LIBRARIES "" )
IF( WIN32 )
INCLUDE( "CMakeLists.win32.txt" )
ENDIF( WIN32 )
IF( UNIX )
INCLUDE( "CMakeLists.unix.txt" )
ENDIF( UNIX )
IF( APPLE )
INCLUDE( "CMakeLists.osx.txt" )
ENDIF( APPLE )
IF( UNIX AND NOT APPLE )
INCLUDE( "CMakeLists.linux.txt" )
ENDIF( UNIX AND NOT APPLE )
qt4_add_resources( RC_SRCS "../resources.qrc" )
qt4_wrap_cpp( tomahawkMoc ${tomahawkHeaders} )
SET( final_src ${final_src} ${tomahawkMoc} ${tomahawkSources} ${tomahawkHeaders} )
IF( "${gui}" STREQUAL "no" )
ELSE()
qt4_wrap_ui( tomahawkUI_H ${tomahawkUI} )
qt4_wrap_cpp( tomahawkMocGui ${tomahawkHeadersGui} )
SET( final_src ${final_src} ${tomahawkUI_H} ${tomahawkMocGui} ${tomahawkSourcesGui} ${RC_SRCS} )
ENDIF()
IF( UNIX AND NOT APPLE )
ADD_EXECUTABLE( tomahawk ${final_src} )
ENDIF( UNIX AND NOT APPLE )
IF( APPLE )
ADD_EXECUTABLE( tomahawk MACOSX_BUNDLE ${final_src} )
ENDIF( APPLE )
IF( WIN32 )
ADD_EXECUTABLE( tomahawk ${final_src} )
ENDIF( WIN32 )
MESSAGE( STATUS "OS_SPECIFIC_LINK_LIBRARIES: ${OS_SPECIFIC_LINK_LIBRARIES}" )
TARGET_LINK_LIBRARIES( tomahawk
${QT_LIBRARIES}
${MAC_EXTRA_LIBS}
${OS_SPECIFIC_LINK_LIBRARIES}
portfwd
)
INCLUDE( "CPack.txt" )

28
src/CMakeLists.unix.txt Normal file
View File

@@ -0,0 +1,28 @@
ADD_DEFINITIONS( -ggdb )
ADD_DEFINITIONS( -Wall )
ADD_DEFINITIONS( -g )
ADD_DEFINITIONS( -fno-operator-names )
ADD_DEFINITIONS( -fPIC )
SET( GLOOX_LIBS ${GLOOX_LIBS} resolv gloox.a )
SET( OS_SPECIFIC_LINK_LIBRARIES
${LIBLASTFM_LIBRARY}
${GLOOX_LIBS}
qxtweb-standalone
qjson
tag
)
IF( "${gui}" STREQUAL "no" )
ELSE()
SET( OS_SPECIFIC_LINK_LIBRARIES
${OS_SPECIFIC_LINK_LIBRARIES}
mad
vorbisfile
ogg
)
SET( tomahawkSourcesGui ${tomahawkSourcesGui} audio/vorbistranscode.cpp scrobbler.cpp )
SET( tomahawkHeadersGui ${tomahawkHeadersGui} audio/vorbistranscode.h scrobbler.h )
ENDIF()

51
src/CMakeLists.win32.txt Normal file
View File

@@ -0,0 +1,51 @@
ADD_DEFINITIONS( /DNOMINMAX )
ADD_DEFINITIONS( /DWIN32_LEAN_AND_MEAN )
ADD_DEFINITIONS( -static-libgcc )
ADD_DEFINITIONS( -DNO_LIBLASTFM )
ADD_DEFINITIONS( -DNO_OGG )
# Add manual locations to stuff:
INCLUDE_DIRECTORIES(
../../libmad-0.15.1b
../../boost_1_43_0
../../gloox-1.0
../../qjson
../../liblastfm/_include
../../taglib-1.6.3/
../../taglib-1.6.3/build
../../taglib-1.6.3/taglib
../../taglib-1.6.3/taglib/toolkit
)
SET( OS_SPECIFIC_LINK_LIBRARIES
"${CMAKE_CURRENT_SOURCE_DIR}/../../gloox-1.0/src/.libs/libgloox.a"
"${CMAKE_CURRENT_SOURCE_DIR}/../../qjson/build/lib/libqjson.dll.a"
"${CMAKE_CURRENT_SOURCE_DIR}/../../taglib-1.6.3/build/taglib/libtag.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../../zlib-1.2.3/lib/libz.a"
"secur32.dll"
"Crypt32.dll"
"ws2_32.dll"
"dnsapi.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../qxtweb-standalone/libqxtweb-standalone.dll"
)
SET( OS_SPECIFIC_LINK_LIBRARIES
${OS_SPECIFIC_LINK_LIBRARIES}
"${CMAKE_CURRENT_SOURCE_DIR}/../admin/win/tomahawk.res"
)
IF( "${gui}" STREQUAL "no" )
ELSE()
SET( tomahawkSourcesGui ${tomahawkSourcesGui}
audio/rtaudiooutput.cpp
)
SET( tomahawkHeadersGui ${tomahawkHeadersGui} audio/rtaudiooutput.h )
SET( OS_SPECIFIC_LINK_LIBRARIES
${OS_SPECIFIC_LINK_LIBRARIES}
"dsound.dll"
"winmm.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../rtaudio/librtaudio.dll"
"${CMAKE_CURRENT_SOURCE_DIR}/../admin/win/dlls/libmad.dll"
)
ENDIF()

70
src/CPack.txt Normal file
View File

@@ -0,0 +1,70 @@
# cd build ; cmake -DCPACK_GENERATOR=DEB .. ; make package
INCLUDE(InstallRequiredSystemLibraries)
set(CPACK_PACKAGING_INSTALL_PREFIX /usr/local)
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Tomahawk desktop player")
SET(CPACK_PACKAGE_NAME "tomahawk")
SET(CPACK_PACKAGE_VENDOR "tomahawk.org")
SET(CPACK_PACKAGE_CONTACT "Richard Jones")
SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "rj@tomahawk.org")
SET(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../README")
SET(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE.txt")
SET(CPACK_PACKAGE_VERSION_MAJOR "0")
SET(CPACK_PACKAGE_VERSION_MINOR "1")
SET(CPACK_PACKAGE_VERSION_PATCH "3")
SET(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
#SET(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "i386") # Default: Output of dpkg --print-architecture or i386
# Copied from generator script, needs to be set for inclusion into filename of package:
# $ dpkg --print-architecture
FIND_PROGRAM(DPKG_CMD dpkg)
IF(NOT DPKG_CMD)
MESSAGE(STATUS "Can not find dpkg in your path, default to i386.")
SET(CPACK_DEBIAN_PACKAGE_ARCHITECTURE i386)
ELSE(NOT DPKG_CMD)
EXECUTE_PROCESS(COMMAND "${DPKG_CMD}" --print-architecture
OUTPUT_VARIABLE CPACK_DEBIAN_PACKAGE_ARCHITECTURE
OUTPUT_STRIP_TRAILING_WHITESPACE
)
ENDIF(NOT DPKG_CMD)
EXECUTE_PROCESS(COMMAND "date" "+%s"
OUTPUT_VARIABLE TIMEMARK
OUTPUT_STRIP_TRAILING_WHITESPACE
)
# eg: tomahawk-i386-1.0.0
SET(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}-${CPACK_PACKAGE_VERSION}_${TIMEMARK}")
SET(CPACK_PACKAGE_INSTALL_DIRECTORY "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}")
IF(WIN32 AND NOT UNIX)
###
ELSE(WIN32 AND NOT UNIX)
# SET(CPACK_STRIP_FILES "tomahawk")
# SET(CPACK_SOURCE_STRIP_FILES "")
ENDIF(WIN32 AND NOT UNIX)
# Nsis only? SET(CPACK_PACKAGE_EXECUTABLES "tomahawk" "tomahawk")
#gnutls is in here because gloox needs it, and we link statically to gloox:
SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libqtgui4 (>=4:4.6.2-0ubuntu5), libtag1c2a (>=1.6.2-0ubuntu1), liblastfm-dev (>=0.4.0~git20090710-1), libqt4-sql-sqlite (>=4:4.6.2-0ubuntu5), libvorbis0a (>=1.2.3-3ubuntu1), libmad0 (>=0.15.1b-4ubuntu1), libasound2 (>=1.0.22-0ubuntu7), zlib1g (>=1:1.2.3.3.dfsg-15ubuntu1), libqjson-dev (>=0.7.1-1), libgnutls26 (>= 2.7.14-0)")
#SET(CPACK_DEBIAN_PACKAGE_SECTION "music")
INSTALL(
TARGETS tomahawk DESTINATION bin
RUNTIME DESTINATION bin
# LIBRARY DESTINATION lib
# ARCHIVE DESTINATION lib
)
INSTALL(
PROGRAMS ${CMAKE_BINARY_DIR}/tomahawk
DESTINATION bin
)
INCLUDE(CPack)

387
src/audio/audioengine.cpp Normal file
View File

@@ -0,0 +1,387 @@
#include "audioengine.h"
#include <QUrl>
#include <QMutexLocker>
#include <tomahawk/tomahawkapp.h>
#include "madtranscode.h"
#ifndef NO_OGG
#include "vorbistranscode.h"
#endif
AudioEngine::AudioEngine()
: QThread()
, m_playlist( 0 )
, m_i( 0 )
{
qDebug() << "Init AudioEngine";
moveToThread( this );
#ifdef Q_WS_X11
m_audio = new AlsaPlayback();
#else
m_audio = new RTAudioOutput();
#endif
connect( m_audio, SIGNAL( timeElapsed( unsigned int ) ), SLOT( timerTriggered( unsigned int ) ), Qt::DirectConnection );
start();
}
AudioEngine::~AudioEngine()
{
qDebug() << Q_FUNC_INFO << "waiting for event loop to finish..";
quit();
wait( 1000 );
qDebug() << Q_FUNC_INFO << "ok";
m_input.clear();
delete m_audio;
}
void
AudioEngine::play()
{
qDebug() << Q_FUNC_INFO;
if ( m_audio->isPaused() )
{
QMutexLocker lock( &m_mutex );
m_audio->resume();
emit resumed();
}
else
loadNextTrack();
}
void
AudioEngine::pause()
{
qDebug() << Q_FUNC_INFO;
QMutexLocker lock( &m_mutex );
m_audio->pause();
emit paused();
}
void
AudioEngine::stop()
{
qDebug() << Q_FUNC_INFO;
QMutexLocker lock( &m_mutex );
if ( !m_input.isNull() )
{
m_input->close();
m_input.clear();
}
if ( !m_transcode.isNull() )
m_transcode->clearBuffers();
m_audio->stopPlayback();
emit stopped();
}
void
AudioEngine::previous()
{
qDebug() << Q_FUNC_INFO;
loadPreviousTrack();
}
void
AudioEngine::next()
{
qDebug() << Q_FUNC_INFO;
loadNextTrack();
}
void
AudioEngine::setVolume( int percentage )
{
//qDebug() << Q_FUNC_INFO;
if ( percentage > 100 )
percentage = 100;
if ( percentage < 0 )
percentage = 0;
m_audio->setVolume( percentage );
emit volumeChanged( percentage );
}
void
AudioEngine::onTrackAboutToClose()
{
qDebug() << Q_FUNC_INFO;
// the only way the iodev we are reading from closes itself, is if
// there was a failure, usually network went away.
// but we might as well play the remaining data we received
// stop();
}
bool
AudioEngine::loadTrack( PlaylistItem* item )
{
qDebug() << Q_FUNC_INFO << thread() << item;
bool err = false;
// in a separate scope due to the QMutexLocker!
{
QMutexLocker lock( &m_mutex );
QSharedPointer<QIODevice> io;
if ( !item )
err = true;
else
{
m_currentTrack = item->query()->results().at( 0 );
io = TomahawkApp::instance()->getIODeviceForUrl( m_currentTrack );
if ( !io || io.isNull() )
{
qDebug() << "Error getting iodevice for item";
err = true;
}
else
connect( io.data(), SIGNAL( aboutToClose() ), SLOT( onTrackAboutToClose() ), Qt::DirectConnection );
}
if ( !err )
{
qDebug() << "Starting new song from url:" << m_currentTrack->url();
emit loading( m_currentTrack );
if ( !m_input.isNull() )
{
m_input->close();
m_input.clear();
}
if ( !m_transcode.isNull() )
{
m_transcode->clearBuffers();
m_transcode.clear();
}
if ( m_currentTrack->mimetype() == "audio/mpeg" )
{
m_transcode = QSharedPointer<TranscodeInterface>(new MADTranscode());
}
#ifndef NO_OGG
else if ( m_currentTrack->mimetype() == "application/ogg" )
{
m_transcode = QSharedPointer<TranscodeInterface>(new VorbisTranscode());
}
#endif
else
qDebug() << "Could NOT find suitable transcoder! Stopping audio.";
if ( !m_transcode.isNull() )
{
connect( m_transcode.data(), SIGNAL( streamInitialized( long, int ) ), SLOT( setStreamData( long, int ) ), Qt::DirectConnection );
m_input = io;
m_audio->clearBuffers();
if ( m_audio->isPaused() )
m_audio->resume();
}
}
}
if ( err )
{
stop();
return false;
}
// needs to be out of the mutexlocker scope
if ( m_transcode.isNull() )
{
stop();
emit error( AudioEngine::DecodeError );
}
return !m_transcode.isNull();
}
void
AudioEngine::loadPreviousTrack()
{
qDebug() << Q_FUNC_INFO;
if ( !m_playlist )
{
stop();
return;
}
loadTrack( m_playlist->previousItem() );
}
void
AudioEngine::loadNextTrack()
{
qDebug() << Q_FUNC_INFO;
if ( !m_playlist )
{
stop();
return;
}
loadTrack( m_playlist->nextItem() );
}
void
AudioEngine::playItem( PlaylistModelInterface* model, PlaylistItem* item )
{
qDebug() << Q_FUNC_INFO;
m_playlist = model;
loadTrack( item );
}
void
AudioEngine::onPlaylistActivated( PlaylistModelInterface* model )
{
qDebug() << Q_FUNC_INFO;
m_playlist = model;
}
void
AudioEngine::setStreamData( long sampleRate, int channels )
{
qDebug() << Q_FUNC_INFO << sampleRate << channels << thread();
m_audio->initAudio( sampleRate, channels );
if ( m_audio->startPlayback() )
{
emit started( m_currentTrack );
}
else
{
qDebug() << "Can't open device for audio output!";
stop();
emit error( AudioEngine::AudioDeviceError );
}
qDebug() << Q_FUNC_INFO << sampleRate << channels << "done";
}
void
AudioEngine::timerTriggered( unsigned int seconds )
{
emit timerSeconds( seconds );
if ( m_currentTrack->duration() == 0 )
{
emit timerPercentage( 0 );
}
else
{
emit timerPercentage( (unsigned int)( seconds / m_currentTrack->duration() ) );
}
}
void
AudioEngine::run()
{
QTimer::singleShot( 0, this, SLOT( engineLoop() ) );
exec();
qDebug() << "AudioEngine event loop stopped";
}
void
AudioEngine::engineLoop()
{
qDebug() << "AudioEngine thread:" << this->thread();
loop();
}
void
AudioEngine::loop()
{
m_i++;
//if( m_i % 500 == 0 ) qDebug() << Q_FUNC_INFO << thread();
{
QMutexLocker lock( &m_mutex );
/* if ( m_i % 200 == 0 )
{
if ( !m_input.isNull() )
qDebug() << "Outer audio loop" << m_input->bytesAvailable() << m_audio->needData();
}*/
if ( m_i % 10 == 0 && m_audio->isPlaying() )
m_audio->triggerTimers();
if( !m_transcode.isNull() &&
!m_input.isNull() &&
m_input->bytesAvailable() &&
m_audio->needData() &&
!m_audio->isPaused() )
{
//if ( m_i % 50 == 0 )
// qDebug() << "Inner audio loop";
if ( m_transcode->needData() > 0 )
{
QByteArray encdata = m_input->read( 8192 ); //FIXME CONSTANT VALUE
m_transcode->processData( encdata, m_input->atEnd() );
}
if ( m_transcode->haveData() )
{
QByteArray rawdata = m_transcode->data();
m_audio->processData( rawdata );
}
QTimer::singleShot( 0, this, SLOT( loop() ) );
return;
}
}
unsigned int nextdelay = 50;
// are we cleanly at the end of a track, and ready for the next one?
if ( !m_input.isNull() &&
m_input->atEnd() &&
m_input->isOpen() &&
!m_input->bytesAvailable() &&
!m_audio->haveData() &&
!m_audio->isPaused() )
{
qDebug() << "Starting next track then";
next();
// will need data immediately:
nextdelay = 0;
}
else if ( !m_input.isNull() && !m_input->isOpen() )
{
qDebug() << "AudioEngine IODev closed. errorString:" << m_input->errorString();
next();
nextdelay = 0;
}
QTimer::singleShot( nextdelay, this, SLOT( loop() ) );
}

94
src/audio/audioengine.h Normal file
View File

@@ -0,0 +1,94 @@
#ifndef AUDIOENGINE_H
#define AUDIOENGINE_H
#include <QThread>
#include <QMutex>
#include <QBuffer>
#include "tomahawk/playlistmodelinterface.h"
#include "tomahawk/result.h"
#include "tomahawk/typedefs.h"
#include "playlistmodel.h"
#include "playlistitem.h"
#include "rtaudiooutput.h"
#include "alsaplayback.h"
#include "transcodeinterface.h"
#define AUDIO_VOLUME_STEP 5
class AudioEngine : public QThread
{
Q_OBJECT
public:
enum AudioErrorCode { StreamReadError, AudioDeviceError, DecodeError };
explicit AudioEngine();
~AudioEngine();
unsigned int volume() { if ( m_audio ) return m_audio->volume() * 100.0; else return 0; }; // in percent
signals:
void loading( const Tomahawk::result_ptr& track );
void started( const Tomahawk::result_ptr& track );
void stopped();
void paused();
void resumed();
void volumeChanged( int volume /* in percent */ );
void timerSeconds( unsigned int secondsElapsed );
void timerPercentage( unsigned int percentage );
void error( AudioErrorCode errorCode );
public slots:
void play();
void pause();
void stop();
void previous();
void next();
void setVolume( int percentage );
void lowerVolume() { setVolume( volume() - AUDIO_VOLUME_STEP ); }
void raiseVolume() { setVolume( volume() + AUDIO_VOLUME_STEP ); }
void onVolumeChanged( float volume ) { emit volumeChanged( volume * 100 ); }
void playItem( PlaylistModelInterface* model, PlaylistItem* item );
void onPlaylistActivated( PlaylistModelInterface* model );
void onTrackAboutToClose();
private slots:
bool loadTrack( PlaylistItem* item );
void loadPreviousTrack();
void loadNextTrack();
void setStreamData( long sampleRate, int channels );
void timerTriggered( unsigned int seconds );
void engineLoop();
void loop();
private:
void run();
QSharedPointer<QIODevice> m_input;
QSharedPointer<TranscodeInterface> m_transcode;
#ifdef Q_WS_X11
AlsaPlayback* m_audio;
#else
RTAudioOutput* m_audio;
#endif
Tomahawk::result_ptr m_currentTrack;
PlaylistModelInterface* m_playlist;
QMutex m_mutex;
int m_i;
};
#endif // AUDIOENGINE_H

225
src/audio/madtranscode.cpp Normal file
View File

@@ -0,0 +1,225 @@
/***************************************************************************
* Copyright (C) 2005 - 2007 by *
* Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm> *
* Erik Jaelevik, Last.fm Ltd <erik@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, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "madtranscode.h"
#include <QDebug>
typedef struct audio_dither
{
mad_fixed_t error[3];
mad_fixed_t random;
} audio_dither;
/* fast 32-bit pseudo-random number generator */
/* code from madplay */
static inline unsigned long prng( unsigned long state )
{
return (state * 0x0019660dL + 0x3c6ef35fL) & 0xffffffffL;
}
/* dithers 24-bit output to 16 bits instead of simple rounding */
/* code from madplay */
static inline signed int dither( mad_fixed_t sample, audio_dither *dither )
{
unsigned int scalebits;
mad_fixed_t output, mask, random;
enum
{
MIN = -MAD_F_ONE,
MAX = MAD_F_ONE - 1
};
/* noise shape */
sample += dither->error[0] - dither->error[1] + dither->error[2];
dither->error[2] = dither->error[1];
dither->error[1] = dither->error[0] / 2;
/* bias */
output = sample + (1L << (MAD_F_FRACBITS + 1 - 16 - 1));
scalebits = MAD_F_FRACBITS + 1 - 16;
mask = (1L << scalebits) - 1;
/* dither */
random = prng(dither->random);
output += (random & mask) - (dither->random & mask);
dither->random = random;
/* clip */
/* TODO: better clipping function */
if (sample >= MAD_F_ONE)
sample = MAD_F_ONE - 1;
else if (sample < -MAD_F_ONE)
sample = -MAD_F_ONE;
if (output >= MAD_F_ONE)
output = MAD_F_ONE - 1;
else if (output < -MAD_F_ONE)
output = -MAD_F_ONE;
/* quantize */
output &= ~mask;
/* error feedback */
dither->error[0] = sample - output;
/* scale */
return output >> scalebits;
}
MADTranscode::MADTranscode() :
m_decodedBufferCapacity( 32 * 1024 ),
m_mpegInitialised( false )
{
qDebug() << "Initialising MAD Transcoding";
mad_stream_init( &stream );
mad_frame_init( &frame );
mad_synth_init( &synth );
timer = mad_timer_zero;
last_timer = mad_timer_zero;
}
MADTranscode::~MADTranscode()
{
qDebug() << Q_FUNC_INFO;
mad_synth_finish( &synth );
mad_frame_finish( &frame );
mad_stream_finish( &stream );
}
void
MADTranscode::processData( const QByteArray &buffer, bool finish )
{
static audio_dither left_dither, right_dither;
int err = 0;
m_encodedBuffer.append( buffer );
while ( err == 0 && ( m_encodedBuffer.count() >= MP3_BUFFER || finish ) )
{
mad_stream_buffer( &stream, (const unsigned char*)m_encodedBuffer.data(), m_encodedBuffer.count() );
err = mad_frame_decode( &frame, &stream );
if ( stream.next_frame != 0 )
{
size_t r = stream.next_frame - stream.buffer;
m_encodedBuffer.remove( 0, r );
}
if ( err )
{
// if ( stream.error != MAD_ERROR_LOSTSYNC )
// qDebug() << "libmad error:" << mad_stream_errorstr( &stream );
if ( !MAD_RECOVERABLE( stream.error ) )
return;
err = 0;
}
else
{
mad_timer_add( &timer, frame.header.duration );
mad_synth_frame( &synth, &frame );
if ( !m_mpegInitialised )
{
long sampleRate = synth.pcm.samplerate;
int channels = synth.pcm.channels;
qDebug() << "madTranscode( Samplerate:" << sampleRate << "- Channels:" << channels << ")";
m_mpegInitialised = true;
emit streamInitialized( sampleRate, channels > 0 ? channels : 2 );
}
for ( int i = 0; i < synth.pcm.length; i++ )
{
union PCMDATA
{
short i;
unsigned char b[2];
} pcmData;
pcmData.i = dither( synth.pcm.samples[0][i], &left_dither );
m_decodedBuffer.append( pcmData.b[0] );
m_decodedBuffer.append( pcmData.b[1] );
if ( synth.pcm.channels == 2 )
{
pcmData.i = dither( synth.pcm.samples[1][i], &right_dither );
m_decodedBuffer.append( pcmData.b[0] );
m_decodedBuffer.append( pcmData.b[1] );
}
}
if ( timer.seconds != last_timer.seconds )
emit timeChanged( timer.seconds );
last_timer = timer;
}
}
}
void
MADTranscode::onSeek( int seconds )
{
mad_timer_t t;
t.seconds = seconds;
t.fraction = 0;
timer = mad_timer_zero;
mad_timer_add( &timer, t );
m_encodedBuffer.clear();
m_decodedBuffer.clear();
}
void
MADTranscode::clearBuffers()
{
mad_synth_finish( &synth );
mad_frame_finish( &frame );
mad_stream_finish( &stream );
m_mpegInitialised = false;
timer = mad_timer_zero;
last_timer = mad_timer_zero;
m_encodedBuffer.clear();
m_decodedBuffer.clear();
mad_stream_init( &stream );
mad_frame_init( &frame );
mad_synth_init( &synth );
}

80
src/audio/madtranscode.h Normal file
View File

@@ -0,0 +1,80 @@
/***************************************************************************
* Copyright (C) 2005 - 2007 by *
* Christian Muehlhaeuser, Last.fm Ltd <chris@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, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*! \class MadTranscode
\brief Transcoding plugin for MP3 streams, using libmad.
*/
#ifndef MADTRANSCODE_H
#define MADTRANSCODE_H
#include "transcodeinterface.h"
#include "mad.h"
#include <QStringList>
#include <QByteArray>
#include <QObject>
#include <QMutex>
#define MP3_BUFFER 32768
class MADTranscode : public TranscodeInterface
{
Q_OBJECT
public:
MADTranscode();
virtual ~MADTranscode();
const QStringList supportedTypes() const { QStringList l; l << "application/x-mp3" << "mp3"; return l; }
int needData() { return MP3_BUFFER - m_encodedBuffer.count(); }
bool haveData() { return !m_decodedBuffer.isEmpty(); }
QByteArray data() { QByteArray b = m_decodedBuffer; m_decodedBuffer.clear(); return b; }
virtual void setBufferCapacity( int bytes ) { m_decodedBufferCapacity = bytes; }
int bufferSize() { return m_decodedBuffer.size(); }
public slots:
virtual void clearBuffers();
virtual void onSeek( int seconds );
virtual void processData( const QByteArray& data, bool finish );
signals:
void streamInitialized( long sampleRate, int channels );
void timeChanged( int seconds );
private:
QByteArray m_encodedBuffer;
QByteArray m_decodedBuffer;
int m_decodedBufferCapacity;
bool m_mpegInitialised;
struct mad_decoder decoder;
struct mad_stream stream;
struct mad_frame frame;
struct mad_synth synth;
mad_timer_t timer;
mad_timer_t last_timer;
};
#endif

262
src/audio/rtaudiooutput.cpp Normal file
View File

@@ -0,0 +1,262 @@
#include <QMutexLocker>
#include <QStringList>
#include <QMessageBox>
#include <QDebug>
#include "rtaudiooutput.h"
#define BUFFER_SIZE 512
int
audioCallback( void *outputBuffer, void *inputBuffer, unsigned int bufferSize, double streamTime, RtAudioStreamStatus status, void* data_src )
{
RTAudioOutput* parent = (RTAudioOutput*)data_src;
QMutexLocker locker( parent->mutex() );
char* buffer = (char*)outputBuffer;
if ( !buffer || bufferSize != BUFFER_SIZE )
return 0;
int bufs = bufferSize * 2 * parent->sourceChannels();
memset( buffer, 0, bufs );
if ( parent->buffer()->size() >= bufs && !parent->isPaused() )
{
// Apply volume scaling
for ( int i = 0; i < bufs / 2; i++ )
{
union PCMDATA
{
short i;
unsigned char b[2];
} pcmData;
pcmData.b[0] = parent->buffer()->at( i * 2 );
pcmData.b[1] = parent->buffer()->at( i * 2 + 1 );
float pcmValue = (float)pcmData.i * parent->volume();
pcmData.i = (short)pcmValue;
buffer[i * 2] = pcmData.b[0];
buffer[i * 2 + 1] = pcmData.b[1];
}
parent->m_pcmCounter += bufs;
parent->buffer()->remove( 0, bufs );
}
return 0;
}
RTAudioOutput::RTAudioOutput() :
m_pcmCounter( 0 ),
m_audio( new RtAudio() ),
m_bufferEmpty( true ),
m_volume( 0.75 ),
m_paused( false ),
m_playing( false ),
m_bps( -1 )
{
qDebug() << Q_FUNC_INFO << m_audio->getCurrentApi();
devices();
}
RTAudioOutput::~RTAudioOutput()
{
qDebug() << Q_FUNC_INFO;
}
QStringList
RTAudioOutput::soundSystems()
{
QStringList l;
#ifdef WIN32
l << "DirectSound";
#endif
#ifdef Q_WS_X11
l << "Alsa";
#endif
#ifdef Q_WS_MAC
l << "CoreAudio";
#endif
return l;
}
QStringList
RTAudioOutput::devices()
{
qDebug() << Q_FUNC_INFO;
QStringList l;
try
{
qDebug() << "Device nums:" << m_audio->getDeviceCount();
for ( unsigned int i = 0; i < m_audio->getDeviceCount(); i++ )
{
RtAudio::DeviceInfo info;
info = m_audio->getDeviceInfo( i );
qDebug() << "Device found:" << i << QString::fromStdString( info.name ) << info.outputChannels << info.duplexChannels << info.isDefaultOutput;
if ( info.outputChannels > 0 )
l << QString::fromStdString( info.name ); // FIXME make it utf8 compatible
}
}
catch ( RtError &error )
{
}
return l;
}
bool
RTAudioOutput::startPlayback()
{
qDebug () << Q_FUNC_INFO;
if ( m_audio->isStreamOpen() )
{
m_audio->startStream();
m_playing = true;
}
return m_audio->isStreamOpen();
}
void
RTAudioOutput::stopPlayback()
{
qDebug() << Q_FUNC_INFO;
QMutexLocker locker( &m_mutex );
delete m_audio; // FIXME
m_audio = new RtAudio();
m_buffer.clear();
m_paused = false;
m_playing = false;
m_bps = -1;
m_pcmCounter = 0;
}
void
RTAudioOutput::initAudio( long sampleRate, int channels )
{
qDebug() << Q_FUNC_INFO << sampleRate << channels;
QMutexLocker locker( &m_mutex );
try
{
delete m_audio;
m_audio = new RtAudio();
m_bps = sampleRate * channels * 2;
m_pcmCounter = 0;
RtAudio::StreamParameters parameters;
parameters.deviceId = m_audio->getDefaultOutputDevice();
parameters.nChannels = channels;
parameters.firstChannel = 0;
unsigned int bufferFrames = BUFFER_SIZE;
RtAudio::StreamOptions options;
options.numberOfBuffers = 32;
//options.flags = RTAUDIO_SCHEDULE_REALTIME;
m_sourceChannels = channels;
m_buffer.clear();
/* if ( m_audio->isStreamRunning() )
m_audio->abortStream();
if ( m_audio->isStreamOpen() )
m_audio->closeStream();*/
m_audio->openStream( &parameters, NULL, RTAUDIO_SINT16, sampleRate, &bufferFrames, &audioCallback, this, &options );
}
catch ( RtError &error )
{
qDebug() << "Starting stream failed. RtAudio error type: " << error.getType();
}
}
bool
RTAudioOutput::needData()
{
if ( m_buffer.isEmpty() && !m_bufferEmpty )
{
m_bufferEmpty = true;
emit bufferEmpty();
}
return ( m_buffer.size() < 65535 ); // FIXME constant value
}
void
RTAudioOutput::processData( const QByteArray &buffer )
{
QMutexLocker locker( &m_mutex );
m_buffer.append( buffer );
if ( m_bufferEmpty && !buffer.isEmpty() )
{
m_bufferEmpty = false;
emit bufferFull();
}
}
void
RTAudioOutput::clearBuffers()
{
qDebug() << Q_FUNC_INFO;
QMutexLocker locker( &m_mutex );
m_buffer.clear();
m_bufferEmpty = true;
emit bufferEmpty();
}
int
RTAudioOutput::internalSoundCardID( int settingsID )
{
if ( settingsID < 0 )
settingsID = 0;
try
{
int card = 0;
for ( unsigned int i = 1; i <= m_audio->getDeviceCount(); i++ )
{
RtAudio::DeviceInfo info;
info = m_audio->getDeviceInfo( i );
if ( info.outputChannels > 0 )
{
if ( card++ == settingsID )
return i;
}
}
}
catch ( RtError &error )
{
}
#ifdef Q_WS_MAC
return 3; // FIXME?
#endif
return 1;
}

71
src/audio/rtaudiooutput.h Normal file
View File

@@ -0,0 +1,71 @@
#ifndef RTAUDIOPLAYBACK_H
#define RTAUDIOPLAYBACK_H
#include "RtAudio.h"
#include <QObject>
#include <QMutex>
class RTAudioOutput : public QObject
{
Q_OBJECT
public:
RTAudioOutput();
~RTAudioOutput();
void initAudio( long sampleRate, int channels );
float volume() { return m_volume; }
bool isPaused() { return m_paused; }
virtual bool isPlaying() { return m_playing; }
bool haveData() { return m_buffer.length() > 0; }
bool needData();
void processData( const QByteArray &buffer );
QStringList soundSystems();
QStringList devices();
int sourceChannels() { return m_sourceChannels; }
QMutex* mutex() { return &m_mutex; }
QByteArray* buffer() { return &m_buffer; }
int m_pcmCounter;
public slots:
void clearBuffers();
bool startPlayback();
void stopPlayback();
void pause() { m_paused = true; }
void resume() { m_paused = false; }
void setVolume( int volume ) { m_volume = ((float)(volume)) / (float)100.0; emit volumeChanged( m_volume ); }
virtual void triggerTimers() { if ( m_bps > 0 ) emit timeElapsed( m_pcmCounter / m_bps ); else emit timeElapsed( 0 ); }
signals:
void bufferEmpty();
void bufferFull();
void volumeChanged( float volume );
void timeElapsed( unsigned int seconds );
private:
RtAudio *m_audio;
bool m_bufferEmpty;
float m_volume;
QByteArray m_buffer;
QMutex m_mutex;
int m_sourceChannels;
bool m_paused;
bool m_playing;
int m_bps;
int internalSoundCardID( int settingsID );
};
#endif

View File

@@ -0,0 +1,32 @@
#ifndef TRANSCODEINTERFACE_H
#define TRANSCODEINTERFACE_H
#include <QStringList>
#include <QByteArray>
#include <QObject>
#include <QMutex>
class TranscodeInterface : public QObject
{
Q_OBJECT
public:
virtual ~TranscodeInterface() {}
virtual const QStringList supportedTypes() const = 0;
virtual int needData() = 0;
virtual bool haveData() = 0;
virtual QByteArray data() = 0;
// virtual void setBufferCapacity( int bytes ) = 0;
// virtual int bufferSize() = 0;
public slots:
virtual void clearBuffers() = 0;
virtual void onSeek( int seconds ) = 0;
virtual void processData( const QByteArray& data, bool finish ) = 0;
};
#endif

View File

@@ -0,0 +1,140 @@
/***************************************************************************
* Copyright (C) 2005 - 2006 by *
* Christian Muehlhaeuser, Last.fm Ltd <chris@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, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. *
***************************************************************************/
#include "vorbistranscode.h"
size_t
vorbis_read( void* data_ptr, size_t byteSize, size_t sizeToRead, void* data_src )
{
VorbisTranscode* parent = (VorbisTranscode*)data_src;
QMutexLocker locker( parent->mutex() );
int r = byteSize * sizeToRead;
if ( r > parent->buffer()->size() )
r = parent->buffer()->size();
memcpy( data_ptr, (char*)parent->buffer()->data(), r );
parent->buffer()->remove( 0, r );
return r;
}
int
vorbis_seek( void* data_src, ogg_int64_t offset, int origin )
{
return -1;
}
int
vorbis_close( void* data_src )
{
// done ;-)
return 0;
}
long
vorbis_tell( void* data_src )
{
return -1;
}
VorbisTranscode::VorbisTranscode()
: m_vorbisInit( false )
{
qDebug() << Q_FUNC_INFO;
}
VorbisTranscode::~VorbisTranscode()
{
qDebug() << Q_FUNC_INFO;
}
void
VorbisTranscode::onSeek( int seconds )
{
QMutexLocker locker( &m_mutex );
m_buffer.clear();
m_outBuffer.clear();
}
void
VorbisTranscode::clearBuffers()
{
QMutexLocker locker( &m_mutex );
m_vorbisInit = false;
m_buffer.clear();
m_outBuffer.clear();
}
void
VorbisTranscode::processData( const QByteArray& data, bool )
{
m_mutex.lock();
m_buffer.append( data );
m_mutex.unlock();
if ( !m_vorbisInit && m_buffer.size() >= OGG_BUFFER )
{
ov_callbacks oggCallbacks;
oggCallbacks.read_func = vorbis_read;
oggCallbacks.close_func = vorbis_close;
oggCallbacks.seek_func = vorbis_seek;
oggCallbacks.tell_func = vorbis_tell;
ov_open_callbacks( this, &m_vorbisFile, 0, 0, oggCallbacks );
m_vorbisInit = true;
// Try to determine samplerate
vorbis_info* vi = ov_info( &m_vorbisFile, -1 );
qDebug() << "vorbisTranscode( Samplerate:" << vi->rate << "Channels:" << vi->channels << ")";
emit streamInitialized( vi->rate, vi->channels );
}
long result = 1;
int currentSection = 0;
while ( m_buffer.size() >= OGG_BUFFER && result > 0 )
{
char tempBuffer[16384];
result = ov_read( &m_vorbisFile, tempBuffer, sizeof( tempBuffer ), 0, 2, 1, &currentSection );
if ( result > 0 )
{
for ( int i = 0; i < ( result / 2 ); i++ )
{
m_outBuffer.append( tempBuffer[i * 2] );
m_outBuffer.append( tempBuffer[i * 2 + 1] );
}
}
}
}

View File

@@ -0,0 +1,78 @@
/***************************************************************************
* Copyright (C) 2005 - 2006 by *
* Christian Muehlhaeuser, Last.fm Ltd <chris@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, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307, USA. *
***************************************************************************/
/*! \class VorbisTranscode
\brief Transcoding plugin for OGG/Vorbis streams.
*/
#ifndef VORBIS_TRANSCODE_H
#define VORBIS_TRANSCODE_H
#include "transcodeinterface.h"
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>
#include <QObject>
#include <QMutex>
#include <QDebug>
#include <QStringList>
// Must not be smaller than 8500 bytes!
#define OGG_BUFFER 8500
class VorbisTranscode : public TranscodeInterface
{
Q_OBJECT
public:
VorbisTranscode();
~VorbisTranscode();
const QStringList supportedTypes() const { QStringList l; l << "application/ogg" << "ogg"; return l; }
int needData() { return OGG_BUFFER - m_buffer.count(); }
bool haveData() { return !m_outBuffer.isEmpty(); }
QByteArray data() { QByteArray b = m_outBuffer; m_outBuffer.clear(); return b; }
QMutex* mutex() { return &m_mutex; }
QByteArray* buffer() { return &m_buffer; }
public slots:
void clearBuffers();
void onSeek( int seconds );
void processData( const QByteArray& data, bool finish );
signals:
void streamInitialized( long sampleRate, int channels );
void timeChanged( int seconds );
private:
QByteArray m_outBuffer;
QMutex m_mutex;
QByteArray m_buffer;
OggVorbis_File m_vorbisFile;
bool m_vorbisInit;
};
#endif

405
src/audiocontrols.cpp Normal file
View File

@@ -0,0 +1,405 @@
#include "audiocontrols.h"
#include "ui_audiocontrols.h"
#include <QNetworkReply>
#include "tomahawk/tomahawkapp.h"
#include "utils/tomahawkutils.h"
#include "audioengine.h"
#include "imagebutton.h"
#include "playlistproxymodel.h"
#include "playlistview.h"
#define LASTFM_DEFAULT_COVER "http://cdn.last.fm/flatness/catalogue/noimage"
AudioControls::AudioControls( QWidget* parent )
: QWidget( parent )
, ui( new Ui::AudioControls )
, m_repeatMode( PlaylistModelInterface::NoRepeat )
, m_shuffled( false )
{
ui->setupUi( this );
ui->buttonAreaLayout->setSpacing( 2 );
ui->trackLabelLayout->setSpacing( 3 );
QFont font( ui->artistTrackLabel->font() );
font.setPixelSize( 12 );
ui->artistTrackLabel->setFont( font );
ui->albumLabel->setFont( font );
ui->timeLabel->setFont( font );
ui->timeLeftLabel->setFont( font );
font.setPixelSize( 9 );
ui->ownerLabel->setFont( font );
ui->prevButton->setPixmap( RESPATH "images/back-rest.png" );
ui->prevButton->setPixmap( RESPATH "images/back-pressed.png", QIcon::Off, QIcon::Active );
ui->playPauseButton->setPixmap( RESPATH "images/play-rest.png" );
ui->playPauseButton->setPixmap( RESPATH "images/play-pressed.png", QIcon::Off, QIcon::Active );
ui->pauseButton->setPixmap( RESPATH "images/pause-rest.png" );
ui->pauseButton->setPixmap( RESPATH "images/pause-pressed.png", QIcon::Off, QIcon::Active );
ui->nextButton->setPixmap( RESPATH "images/skip-rest.png" );
ui->nextButton->setPixmap( RESPATH "images/skip-pressed.png", QIcon::Off, QIcon::Active );
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-rest.png" );
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-pressed.png", QIcon::Off, QIcon::Active );
ui->repeatButton->setPixmap( RESPATH "images/repeat-off-rest.png" );
ui->repeatButton->setPixmap( RESPATH "images/repeat-off-pressed.png", QIcon::Off, QIcon::Active );
ui->volumeLowButton->setPixmap( RESPATH "images/volume-icon-muted.png" );
ui->volumeHighButton->setPixmap( RESPATH "images/volume-icon-full.png" );
ui->ownerLabel->setForegroundRole( QPalette::Dark );
ui->metadataArea->setStyleSheet( "QWidget#metadataArea {\nborder-width: 4px;\nborder-image: url(" RESPATH "images/now-playing-panel.png) 4 4 4 4 stretch stretch; }" );
ui->seekSlider->setFixedHeight( 20 );
ui->seekSlider->setEnabled( false );
ui->seekSlider->setStyleSheet( "QSlider::groove::horizontal {"
"margin: 5px; border-width: 3px;"
"border-image: url(" RESPATH "images/seek-slider-bkg.png) 3 3 3 3 stretch stretch;"
"}"
"QSlider::handle::horizontal {"
"margin-left: 5px; margin-right: -5px; "
"width: 0px;"
//"margin-bottom: -1px; margin-top: -1px; "
//"height: 17px; width: 6px; "
//"background-image: url(" RESPATH "images/seek-and-volume-knob-rest.png);"
//"background-repeat: no-repeat;"
"}"
"QSlider::sub-page:horizontal {"
"margin: 5px; border-width: 3px;"
"border-image: url(" RESPATH "images/seek-slider-level.png) 3 3 3 3 stretch stretch;"
"}"
);
ui->volumeSlider->setFixedHeight( 20 );
ui->volumeSlider->setRange( 0, 100 );
ui->volumeSlider->setValue( APP->audioEngine()->volume() );
ui->volumeSlider->setStyleSheet( "QSlider::groove::horizontal {"
"margin: 5px; border-width: 3px;"
"border-image: url(" RESPATH "images/volume-slider-bkg.png) 3 3 3 3 stretch stretch;}"
"QSlider::sub-page:horizontal {"
"margin: 5px; border-width: 3px;"
"border-image: url(" RESPATH "images/seek-slider-level.png) 3 3 3 3 stretch stretch;"
"}"
"QSlider::handle::horizontal {"
"margin-left: 0px; margin-right: 0px;"
"margin-bottom: -1px; margin-top: -1px; "
"height: 17px; width: 6px; "
"background-image: url(" RESPATH "images/seek-and-volume-knob-rest.png);"
"background-repeat: no-repeat;"
"}"
);
/* m_playAction = new QAction( this );
m_pauseAction = new QAction( this );
m_prevAction = new QAction( this );
m_nextAction = new QAction( this );
connect( m_playAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( play() ) );
connect( m_pauseAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( pause() ) );
connect( m_prevAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( previous() ) );
connect( m_nextAction, SIGNAL( triggered() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( next() ) ); */
connect( ui->volumeSlider, SIGNAL( valueChanged( int ) ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( setVolume( int ) ) );
connect( ui->prevButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( previous() ) );
connect( ui->playPauseButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( play() ) );
connect( ui->pauseButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( pause() ) );
connect( ui->nextButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( next() ) );
connect( ui->volumeLowButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( lowerVolume() ) );
connect( ui->volumeHighButton, SIGNAL( clicked() ), (QObject*)TomahawkApp::instance()->audioEngine(), SLOT( raiseVolume() ) );
connect( ui->repeatButton, SIGNAL( clicked() ), SLOT( onRepeatClicked() ) );
connect( ui->shuffleButton, SIGNAL( clicked() ), SLOT( onShuffleClicked() ) );
// <From AudioEngine>
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( loading( const Tomahawk::result_ptr& ) ), SLOT( onPlaybackLoading( const Tomahawk::result_ptr& ) ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( started( const Tomahawk::result_ptr& ) ), SLOT( onPlaybackStarted( const Tomahawk::result_ptr& ) ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( paused() ), SLOT( onPlaybackPaused() ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( resumed() ), SLOT( onPlaybackResumed() ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( stopped() ), SLOT( onPlaybackStopped() ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( timerSeconds( unsigned int ) ), SLOT( onPlaybackTimer( unsigned int ) ) );
connect( (QObject*)TomahawkApp::instance()->audioEngine(), SIGNAL( volumeChanged( int ) ), SLOT( onVolumeChanged( int ) ) );
m_defaultCover = QPixmap( RESPATH "images/no-album-art-placeholder.png" )
.scaled( ui->coverImage->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation );
onPlaybackStopped(); // initial state
}
AudioControls::~AudioControls()
{
delete ui;
}
void
AudioControls::changeEvent( QEvent* e )
{
QWidget::changeEvent( e );
switch ( e->type() )
{
case QEvent::LanguageChange:
// ui->retranslateUi( this );
break;
default:
break;
}
}
void
AudioControls::onVolumeChanged( int volume )
{
ui->volumeSlider->blockSignals( true );
ui->volumeSlider->setValue( volume );
ui->volumeSlider->blockSignals( false );
}
void
AudioControls::onCoverArtDownloaded()
{
if ( m_currentTrack.isNull() )
return;
QNetworkReply* reply = qobject_cast<QNetworkReply*>( sender() );
QUrl redir = reply->attribute( QNetworkRequest::RedirectionTargetAttribute ).toUrl();
if ( redir.isEmpty() )
{
const QByteArray ba = reply->readAll();
if ( ba.length() )
{
QPixmap pm;
pm.loadFromData( ba );
if ( pm.isNull() || reply->url().toString().startsWith( LASTFM_DEFAULT_COVER ) )
ui->coverImage->setPixmap( m_defaultCover );
else
ui->coverImage->setPixmap( pm.scaled( ui->coverImage->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation ) );
}
}
else
{
// qDebug() << "Following redirect to" << redir.toString();
QNetworkRequest req( redir );
QNetworkReply* reply = APP->nam()->get( req );
connect( reply, SIGNAL( finished() ), SLOT( onCoverArtDownloaded() ) );
}
reply->deleteLater();
}
void
AudioControls::onPlaybackStarted( const Tomahawk::result_ptr& result )
{
qDebug() << Q_FUNC_INFO;
onPlaybackLoading( result );
QString imgurl = "http://ws.audioscrobbler.com/2.0/?method=album.imageredirect&artist=%1&album=%2&size=medium&api_key=7a90f6672a04b809ee309af169f34b8b";
QNetworkRequest req( imgurl.arg( result->artist() ).arg( result->album() ) );
QNetworkReply* reply = APP->nam()->get( req );
connect( reply, SIGNAL( finished() ), SLOT( onCoverArtDownloaded() ) );
}
void
AudioControls::onPlaybackLoading( const Tomahawk::result_ptr& result )
{
qDebug() << Q_FUNC_INFO;
m_currentTrack = result;
ui->artistTrackLabel->setText( QString( "%1 - %2" ).arg( result->artist() ).arg( result->track() ) );
ui->albumLabel->setText( result->album() );
ui->ownerLabel->setText( result->collection()->source()->friendlyName() );
ui->coverImage->setPixmap( m_defaultCover );
if ( ui->timeLabel->text().isEmpty() )
ui->timeLabel->setText( "00:00" );
if ( ui->timeLeftLabel->text().isEmpty() )
ui->timeLeftLabel->setText( "-" + TomahawkUtils::timeToString( result->duration() ) );
ui->seekSlider->setRange( 0, m_currentTrack->duration() );
ui->seekSlider->setVisible( true );
/* m_playAction->setEnabled( false );
m_pauseAction->setEnabled( true ); */
ui->pauseButton->setEnabled( true );
ui->pauseButton->setVisible( true );
ui->playPauseButton->setVisible( false );
ui->playPauseButton->setEnabled( false );
}
void
AudioControls::onPlaybackPaused()
{
/* m_pauseAction->setEnabled( false );
m_playAction->setEnabled( true ); */
ui->pauseButton->setVisible( false );
ui->pauseButton->setEnabled( false );
ui->playPauseButton->setEnabled( true );
ui->playPauseButton->setVisible( true );
}
void
AudioControls::onPlaybackResumed()
{
/* m_playAction->setEnabled( false );
m_pauseAction->setEnabled( true ); */
ui->pauseButton->setVisible( true );
ui->pauseButton->setEnabled( true );
ui->playPauseButton->setVisible( false );
ui->playPauseButton->setEnabled( false );
}
void
AudioControls::onPlaybackStopped()
{
m_currentTrack.clear();
ui->artistTrackLabel->setText( "" );
ui->albumLabel->setText( "" );
ui->ownerLabel->setText( "" );
ui->timeLabel->setText( "" );
ui->timeLeftLabel->setText( "" );
ui->coverImage->setPixmap( QPixmap() );
ui->seekSlider->setVisible( false );
ui->pauseButton->setVisible( false );
ui->pauseButton->setEnabled( false );
ui->playPauseButton->setEnabled( true );
ui->playPauseButton->setVisible( true );
/* m_pauseAction->setEnabled( false );
m_playAction->setEnabled( true ); */
}
void
AudioControls::onPlaybackTimer( unsigned int seconds )
{
if ( m_currentTrack.isNull() )
return;
ui->timeLabel->setText( TomahawkUtils::timeToString( seconds ) );
ui->timeLeftLabel->setText( "-" + TomahawkUtils::timeToString( m_currentTrack->duration() - seconds ) );
ui->seekSlider->setValue( seconds );
}
void
AudioControls::onRepeatModeChanged( PlaylistModelInterface::RepeatMode mode )
{
m_repeatMode = mode;
switch ( m_repeatMode )
{
case PlaylistModelInterface::NoRepeat:
{
// switch to RepeatOne
ui->repeatButton->setPixmap( RESPATH "images/repeat-off-rest.png" );
ui->repeatButton->setPixmap( RESPATH "images/repeat-off-pressed.png", QIcon::Off, QIcon::Active );
}
break;
case PlaylistModelInterface::RepeatOne:
{
// switch to RepeatAll
ui->repeatButton->setPixmap( RESPATH "images/repeat-1-on-rest.png" );
ui->repeatButton->setPixmap( RESPATH "images/repeat-1-on-pressed.png", QIcon::Off, QIcon::Active );
}
break;
case PlaylistModelInterface::RepeatAll:
{
// switch to NoRepeat
ui->repeatButton->setPixmap( RESPATH "images/repeat-all-on-rest.png" );
ui->repeatButton->setPixmap( RESPATH "images/repeat-all-on-pressed.png", QIcon::Off, QIcon::Active );
}
break;
default:
break;
}
}
void
AudioControls::onRepeatClicked()
{
switch ( m_repeatMode )
{
case PlaylistModelInterface::NoRepeat:
{
// switch to RepeatOne
APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::RepeatOne );
}
break;
case PlaylistModelInterface::RepeatOne:
{
// switch to RepeatAll
APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::RepeatAll );
}
break;
case PlaylistModelInterface::RepeatAll:
{
// switch to NoRepeat
APP->playlistView()->model()->setRepeatMode( PlaylistModelInterface::NoRepeat );
}
break;
default:
break;
}
}
void
AudioControls::onShuffleModeChanged( bool enabled )
{
m_shuffled = enabled;
if ( m_shuffled )
{
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-on-rest.png" );
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-on-pressed.png", QIcon::Off, QIcon::Active );
ui->repeatButton->setEnabled( false );
}
else
{
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-rest.png" );
ui->shuffleButton->setPixmap( RESPATH "images/shuffle-off-pressed.png", QIcon::Off, QIcon::Active );
ui->repeatButton->setEnabled( true );
}
}
void
AudioControls::onShuffleClicked()
{
APP->playlistView()->model()->setShuffled( m_shuffled ^ true );
}

58
src/audiocontrols.h Normal file
View File

@@ -0,0 +1,58 @@
#ifndef AUDIOCONTROLS_H
#define AUDIOCONTROLS_H
#include <QWidget>
#include "tomahawk/playlistmodelinterface.h"
#include "tomahawk/result.h"
namespace Ui
{
class AudioControls;
}
class AudioControls : public QWidget
{
Q_OBJECT
public:
AudioControls( QWidget* parent = 0 );
~AudioControls();
public slots:
void onRepeatModeChanged( PlaylistModelInterface::RepeatMode mode );
void onShuffleModeChanged( bool enabled );
protected:
void changeEvent( QEvent* e );
private slots:
void onPlaybackStarted( const Tomahawk::result_ptr& result );
void onPlaybackLoading( const Tomahawk::result_ptr& result );
void onPlaybackPaused();
void onPlaybackResumed();
void onPlaybackStopped();
void onPlaybackTimer( unsigned int seconds );
void onVolumeChanged( int volume );
void onRepeatClicked();
void onShuffleClicked();
void onCoverArtDownloaded();
private:
Ui::AudioControls *ui;
QAction* m_playAction;
QAction* m_pauseAction;
QAction* m_prevAction;
QAction* m_nextAction;
QPixmap m_defaultCover;
Tomahawk::result_ptr m_currentTrack;
PlaylistModelInterface::RepeatMode m_repeatMode;
bool m_shuffled;
};
#endif // AUDIOCONTROLS_H

458
src/audiocontrols.ui Normal file
View File

@@ -0,0 +1,458 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AudioControls</class>
<widget class="QWidget" name="AudioControls">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>706</width>
<height>70</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>70</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>70</height>
</size>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>1</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>1</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="buttonArea" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>254</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>254</width>
<height>16777215</height>
</size>
</property>
<layout class="QHBoxLayout" name="buttonAreaLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="ImageButton" name="prevButton">
<property name="text">
<string>Prev</string>
</property>
</widget>
</item>
<item>
<widget class="ImageButton" name="playPauseButton">
<property name="text">
<string>Play</string>
</property>
</widget>
</item>
<item>
<widget class="ImageButton" name="pauseButton">
<property name="text">
<string>Pause</string>
</property>
</widget>
</item>
<item>
<widget class="ImageButton" name="nextButton">
<property name="text">
<string>Next</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="metadataArea" native="true">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>66</height>
</size>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="leftMargin">
<number>12</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<property name="bottomMargin">
<number>1</number>
</property>
<item>
<widget class="QLabel" name="coverImage">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>49</width>
<height>48</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>49</width>
<height>48</height>
</size>
</property>
<property name="text">
<string>Cover</string>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="margin">
<number>0</number>
</property>
<property name="indent">
<number>-1</number>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>8</number>
</property>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QVBoxLayout" name="trackLabelLayout">
<item>
<widget class="QLabel" name="artistTrackLabel">
<property name="text">
<string>Artist</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="albumLabel">
<property name="text">
<string>Album</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="ownerLabel">
<property name="font">
<font>
<pointsize>7</pointsize>
</font>
</property>
<property name="text">
<string>Owner</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="margin">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="timeLabel">
<property name="text">
<string>Time</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="seekSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeLeftLabel">
<property name="text">
<string>Time Left</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_4" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>200</width>
<height>66</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>2</number>
</property>
<property name="topMargin">
<number>1</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>1</number>
</property>
<item>
<widget class="QWidget" name="widget_6" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<number>2</number>
</property>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="ImageButton" name="shuffleButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Shuffle</string>
</property>
</widget>
</item>
<item>
<widget class="ImageButton" name="repeatButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Repeat</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_5" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<number>2</number>
</property>
<item>
<widget class="ImageButton" name="volumeLowButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Low</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="volumeSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="ImageButton" name="volumeHighButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>High</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ImageButton</class>
<extends>QPushButton</extends>
<header>imagebutton.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

108
src/bufferiodevice.cpp Normal file
View File

@@ -0,0 +1,108 @@
#include <QDebug>
#include "bufferiodevice.h"
BufferIODevice::BufferIODevice( unsigned int size, QObject *parent ) :
QIODevice( parent ),
m_size(size),
m_received(0)
{
}
bool
BufferIODevice::open( OpenMode mode )
{
QMutexLocker lock( &m_mut );
qDebug() << Q_FUNC_INFO;
QIODevice::open( QIODevice::ReadWrite ); // FIXME?
return true;
}
void
BufferIODevice::close()
{
QMutexLocker lock( &m_mut );
qDebug() << Q_FUNC_INFO;
QIODevice::close();
// TODO ?
}
void
BufferIODevice::inputComplete( const QString& errmsg )
{
qDebug() << Q_FUNC_INFO;
setErrorString( errmsg );
emit readChannelFinished();
}
void
BufferIODevice::addData( QByteArray ba )
{
writeData( ba.data(), ba.length() );
}
qint64
BufferIODevice::bytesAvailable() const
{
QMutexLocker lock( &m_mut );
return m_buffer.length();
}
qint64
BufferIODevice::readData( char * data, qint64 maxSize )
{
// qDebug() << Q_FUNC_INFO << maxSize;
QMutexLocker lock( &m_mut );
// qDebug() << "readData begins, bufersize:" << m_buffer.length();
qint64 size = maxSize;
if ( m_buffer.length() < maxSize )
size = m_buffer.length();
memcpy( data, m_buffer.data(), size );
m_buffer.remove( 0, size );
// qDebug() << "readData ends, bufersize:" << m_buffer.length();
return size;
}
qint64 BufferIODevice::writeData( const char * data, qint64 maxSize )
{
{
QMutexLocker lock( &m_mut );
m_buffer.append( data, maxSize );
m_received += maxSize;
}
emit bytesWritten( maxSize );
emit readyRead();
return maxSize;
}
qint64 BufferIODevice::size() const
{
return m_size;
}
bool BufferIODevice::atEnd() const
{
QMutexLocker lock( &m_mut );
return m_size == m_received &&
m_buffer.length() == 0;
}
void
BufferIODevice::clear()
{
QMutexLocker lock( &m_mut );
m_buffer.clear();
}

39
src/bufferiodevice.h Normal file
View File

@@ -0,0 +1,39 @@
#ifndef BUFFERIODEVICE_H
#define BUFFERIODEVICE_H
#include <QIODevice>
#include <QMutexLocker>
#include <QDebug>
class BufferIODevice : public QIODevice
{
Q_OBJECT
public:
explicit BufferIODevice( unsigned int size = 0, QObject *parent = 0 );
virtual bool open( OpenMode mode );
virtual void close();
virtual qint64 bytesAvailable() const;
virtual qint64 size() const;
virtual bool atEnd() const;
void addData( QByteArray ba );
void clear();
bool isOpen() const { qDebug() << "isOpen"; return true; }
OpenMode openMode() const { qDebug() << "openMode"; return QIODevice::ReadWrite; }
void inputComplete( const QString& errmsg = "" );
protected:
virtual qint64 readData( char * data, qint64 maxSize );
virtual qint64 writeData( const char * data, qint64 maxSize );
private:
QByteArray m_buffer;
mutable QMutex m_mut; //const methods need to lock
unsigned int m_size, m_received;
};
#endif // BUFFERIODEVICE_H

118
src/collection.cpp Normal file
View File

@@ -0,0 +1,118 @@
#include "tomahawk/collection.h"
#include <QMetaObject>
#include <QGenericArgument>
#include "tomahawk/playlist.h"
using namespace Tomahawk;
Collection::Collection( const source_ptr& source, const QString& name, QObject* parent )
: QObject( parent )
, m_name( name )
, m_loaded( false )
, m_lastmodified( 0 )
, m_source( source )
{
// qDebug() << Q_FUNC_INFO;
}
Collection::~Collection()
{
qDebug() << Q_FUNC_INFO;
}
void
Collection::invokeSlotTracks( QObject* obj, const char* slotname,
const QList<QVariant>& val,
collection_ptr collection )
{
qDebug() << Q_FUNC_INFO << obj << slotname;
QMetaObject::invokeMethod( obj, slotname, Qt::QueuedConnection,
Q_ARG( QList<QVariant>, val ),
Q_ARG( Tomahawk::collection_ptr, collection ) );
}
QString
Collection::name() const
{
return m_name;
}
void
Collection::addPlaylist( const Tomahawk::playlist_ptr& p )
{
qDebug() << Q_FUNC_INFO;
QList<playlist_ptr> toadd;
toadd << p;
m_playlists.append( toadd );
qDebug() << Q_FUNC_INFO << "Collection name" << name()
<< "from source id" << source()->id()
<< "numplaylists:" << m_playlists.length();
emit playlistsAdded( toadd );
}
void
Collection::deletePlaylist( const Tomahawk::playlist_ptr& p )
{
qDebug() << Q_FUNC_INFO;
QList<playlist_ptr> todelete;
todelete << p;
m_playlists.removeAll( p );
qDebug() << Q_FUNC_INFO << "Collection name" << name()
<< "from source id" << source()->id()
<< "numplaylists:" << m_playlists.length();
emit playlistsDeleted( todelete );
}
void
Collection::loadTracks( QObject* obj, const char* slotname )
{
if ( !obj )
obj = this;
boost::function< void( const QList<QVariant>&, Tomahawk::collection_ptr )> cb =
boost::bind( &Collection::invokeSlotTracks, this, obj, slotname, _1, _2 );
loadAllTracks( cb );
}
playlist_ptr
Collection::playlist( const QString& guid )
{
foreach( const playlist_ptr& pp, m_playlists )
{
if( pp->guid() == guid )
return pp;
}
return playlist_ptr();
}
bool
Collection::trackSorter( const QVariant& left, const QVariant& right )
{
int art = left.toMap().value( "artist" ).toString()
.localeAwareCompare( right.toMap().value( "artist" ).toString() );
if ( art == 0 )
{
int trk = left.toMap().value( "track" ).toString()
.localeAwareCompare( right.toMap().value( "track" ).toString() );
return trk < 0;
}
else
{
return art < 0;
}
}

445
src/connection.cpp Normal file
View File

@@ -0,0 +1,445 @@
#include "connection.h"
#include <QTime>
#include <QThread>
#include "servent.h"
#define PROTOVER "2" // must match remote peer, or we can't talk.
Connection::Connection( Servent* parent )
: QObject()
, m_sock( 0 )
, m_peerport( 0 )
, m_servent( parent )
, m_ready( false )
, m_onceonly( true )
, m_do_shutdown( false )
, m_actually_shutting_down( false )
, m_peer_disconnected( false )
, m_tx_bytes( 0 )
, m_tx_bytes_requested( 0 )
, m_rx_bytes( 0 )
, m_id( "Connection()" )
, m_statstimer( 0 )
, m_stats_tx_bytes_per_sec( 0 )
, m_stats_rx_bytes_per_sec( 0 )
, m_rx_bytes_last( 0 )
, m_tx_bytes_last( 0 )
{
moveToThread( m_servent->thread() );
qDebug() << "CTOR Connection (super)" << thread();
connect( &m_msgprocessor_out, SIGNAL( ready( msg_ptr ) ),
SLOT( sendMsg_now( msg_ptr ) ), Qt::QueuedConnection );
connect( &m_msgprocessor_in, SIGNAL( ready( msg_ptr ) ),
SLOT( handleMsg( msg_ptr ) ), Qt::QueuedConnection );
connect( &m_msgprocessor_in, SIGNAL( empty() ),
SLOT( handleIncomingQueueEmpty() ), Qt::QueuedConnection );
}
Connection::~Connection()
{
qDebug() << "DTOR connection (super)" << id() << thread();
if( !m_sock.isNull() )
{
qDebug() << "deleteLatering sock" << m_sock;
m_sock->deleteLater();
}
else
{
qDebug() << "no valid sock to delete";
}
delete m_statstimer;
}
void
Connection::handleIncomingQueueEmpty()
{
//qDebug() << Q_FUNC_INFO << "bavail" << m_sock->bytesAvailable()
// << "isopen" << m_sock->isOpen()
// << "m_peer_disconnected" << m_peer_disconnected
// << "bytes rx" << bytesReceived();
if( m_sock->bytesAvailable() == 0 && m_peer_disconnected )
{
qDebug() << "No more data to read, peer disconnected. shutting down connection."
<< "bytesavail" << m_sock->bytesAvailable()
<< "bytesrx" << m_rx_bytes;
shutdown();
}
}
// convenience:
void
Connection::setFirstMessage( const QVariant& m )
{
QJson::Serializer ser;
const QByteArray ba = ser.serialize( m );
//qDebug() << "first msg json len:" << ba.length();
setFirstMessage( Msg::factory( ba, Msg::JSON ) );
}
void
Connection::setFirstMessage( msg_ptr m )
{
m_firstmsg = m;
//qDebug() << id() << " first msg set to " << QString::fromAscii(m_firstmsg->payload())
// << "msg len:" << m_firstmsg->length() ;
}
void
Connection::shutdown( bool waitUntilSentAll )
{
qDebug() << Q_FUNC_INFO << waitUntilSentAll;
if(m_do_shutdown)
{
//qDebug() << id() << " already shutting down";
return;
}
m_do_shutdown = true;
if( !waitUntilSentAll )
{
qDebug() << "Shutting down immediately " << id();
actualShutdown();
}
else
{
qDebug() << "Shutting down after transfer complete " << id()
<< "Actual/Desired" << m_tx_bytes << m_tx_bytes_requested;
bytesWritten( 0 ); // trigger shutdown if we've already sent everything
// otherwise the bytesWritten slot will call actualShutdown()
// once all enqueued data has been properly written to the socket
}
}
void
Connection::actualShutdown()
{
qDebug() << Q_FUNC_INFO;
if( m_actually_shutting_down )
{
qDebug() << "(already actually shutting down)";
return;
}
m_actually_shutting_down = true;
if( !m_sock.isNull() && m_sock->isOpen() )
{
m_sock->disconnectFromHost();
}
qDebug() << "EMITTING finished()";
emit finished();
}
void
Connection::markAsFailed()
{
qDebug() << "Connection" << id() << "FAILED ***************" << thread();
emit failed();
shutdown();
}
void
Connection::start( QTcpSocket* sock )
{
Q_ASSERT( m_sock.isNull() );
Q_ASSERT( sock );
Q_ASSERT( sock->isValid() );
m_sock = sock;
if( m_name.isEmpty() )
{
m_name = QString( "peer[%1]" ).arg( m_sock->peerAddress().toString() );
}
QTimer::singleShot( 0, this, SLOT( doSetup() ) );
}
void
Connection::authCheckTimeout()
{
if( m_ready )
return;
qDebug() << "Closing connection, not authed in time.";
shutdown();
}
void
Connection::doSetup()
{
qDebug() << Q_FUNC_INFO << thread();
/*
New connections can be created from other thread contexts, such as
when AudioEngine calls getIODevice.. - we need to ensure that connections
and their associated sockets are running in the same thread as the servent.
HINT: export QT_FATAL_WARNINGS=1 helps to catch these kind of errors.
*/
if( QThread::currentThread() != m_servent->thread() )
{
// Connections should always be in the same thread as the servent.
qDebug() << "Fixing thead affinity...";
moveToThread( m_servent->thread() );
qDebug() << Q_FUNC_INFO << thread();
}
//stats timer calculates BW used by this connection
m_statstimer = new QTimer;
m_statstimer->moveToThread( this->thread() );
m_statstimer->setInterval(1000);
connect( m_statstimer, SIGNAL( timeout() ), SLOT( calcStats() ) );
m_statstimer->start();
m_statstimer_mark.start();
m_sock->moveToThread( thread() );
qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) );
connect( m_sock.data(), SIGNAL(bytesWritten(qint64)),
SLOT(bytesWritten(qint64)), Qt::QueuedConnection);
connect( m_sock.data(), SIGNAL(disconnected()),
SLOT(socketDisconnected()), Qt::QueuedConnection);
connect( m_sock.data(), SIGNAL(error(QAbstractSocket::SocketError)),
SLOT(socketDisconnectedError(QAbstractSocket::SocketError)), Qt::QueuedConnection );
connect( m_sock.data(), SIGNAL(readyRead()),
SLOT(readyRead()), Qt::QueuedConnection);
// if connection not authed/setup fast enough, kill it:
QTimer::singleShot( AUTH_TIMEOUT, this, SLOT( authCheckTimeout() ) );
if( outbound() )
{
Q_ASSERT( !m_firstmsg.isNull() );
sendMsg( m_firstmsg );
}
else
{
sendMsg( Msg::factory( PROTOVER, Msg::SETUP ) );
}
// call readyRead incase we missed the signal in between the servent disconnecting and us
// connecting to the signal - won't do anything if there are no bytesAvailable anyway.
readyRead();
}
void
Connection::socketDisconnected()
{
qDebug() << "SOCKET DISCONNECTED" << this->name()
<< "shutdown will happen after incoming queue empties."
<< "bytesavail:" << m_sock->bytesAvailable()
<< "bytesRecvd" << bytesReceived();
m_peer_disconnected = true;
emit socketClosed();
if( m_msgprocessor_in.length() == 0 && m_sock->bytesAvailable() == 0 )
{
handleIncomingQueueEmpty();
}
}
void
Connection::socketDisconnectedError(QAbstractSocket::SocketError e)
{
qDebug() << "SOCKET ERROR CODE" << e << this->name() << " CALLING Connection::shutdown(false)";
m_peer_disconnected = true;
emit socketErrored(e);
shutdown(false);
}
QString
Connection::id() const
{
return m_id;
}
void
Connection::setId( const QString& id )
{
m_id = id;
}
void
Connection::readyRead()
{
//qDebug() << "readyRead, m_bs:" << m_bs << "bytesavail:" << m_sock->bytesAvailable();
if( m_msg.isNull() )
{
if( m_sock->bytesAvailable() < Msg::headerSize() )
return;
char msgheader[ Msg::headerSize() ];
if( m_sock->read( (char*) &msgheader, Msg::headerSize() ) != Msg::headerSize() )
{
qDebug() << "Failed reading msg header";
this->markAsFailed();
return;
}
m_msg = Msg::begin( (char*) &msgheader );
m_rx_bytes += Msg::headerSize();
}
if( m_sock->bytesAvailable() < m_msg->length() )
return;
QByteArray ba = m_sock->read( m_msg->length() );
if( ba.length() != (qint32)m_msg->length() )
{
qDebug() << "Failed to read full msg payload";
this->markAsFailed();
return;
}
m_msg->fill( ba );
m_rx_bytes += ba.length();
handleReadMsg(); // process m_msg and clear() it
// since there is no explicit threading, use the event loop to schedule this:
if( m_sock->bytesAvailable() )
{
QTimer::singleShot( 0, this, SLOT( readyRead() ) );
}
}
void
Connection::handleReadMsg()
{
if( outbound() == false &&
m_msg->is( Msg::SETUP ) &&
m_msg->payload() == "ok" )
{
m_ready = true;
qDebug() << "Connection" << id() << "READY";
setup();
emit ready();
}
else if( !m_ready &&
outbound() &&
m_msg->is( Msg::SETUP ) )
{
if( m_msg->payload() == PROTOVER )
{
sendMsg( Msg::factory( "ok", Msg::SETUP ) );
m_ready = true;
qDebug() << "Connection" << id() << "READY";
setup();
emit ready();
}
else
{
sendMsg( Msg::factory( "{\"method\":\"protovercheckfail\"}", Msg::JSON | Msg::SETUP ) );
shutdown( true );
}
}
else
{
m_msgprocessor_in.append( m_msg );
}
m_msg.clear();
}
void
Connection::sendMsg( QVariant j )
{
if( m_do_shutdown )
return;
QJson::Serializer serializer;
const QByteArray payload = serializer.serialize( j );
sendMsg( Msg::factory( payload, Msg::JSON ) );
}
void
Connection::sendMsg( msg_ptr msg )
{
if( m_do_shutdown )
{
qDebug() << Q_FUNC_INFO << "SHUTTING DOWN, NOT SENDING msg flags:"
<< (int)msg->flags() << "length:" << msg->length();
return;
}
m_tx_bytes_requested += msg->length() + Msg::headerSize();
m_msgprocessor_out.append( msg );
}
void
Connection::sendMsg_now( msg_ptr msg )
{
//qDebug() << Q_FUNC_INFO << thread() << QThread::currentThread();
Q_ASSERT( QThread::currentThread() == thread() );
Q_ASSERT( this->isRunning() );
if( m_sock.isNull() || !m_sock->isOpen() || !m_sock->isWritable() )
{
qDebug() << "***** Socket problem, whilst in sendMsg(). Cleaning up. *****";
shutdown( true );
return;
}
if( ! msg->write( m_sock.data() ) )
{
//qDebug() << "Error writing to socket in sendMsg() *************";
shutdown( false );
return;
}
}
void
Connection::bytesWritten( qint64 i )
{
m_tx_bytes += i;
// if we are waiting to shutdown, and have sent all queued data, do actual shutdown:
if( m_do_shutdown && m_tx_bytes == m_tx_bytes_requested )
actualShutdown();
}
void
Connection::calcStats()
{
int elapsed = m_statstimer_mark.restart(); // ms since last calc
m_stats_tx_bytes_per_sec = (float)1000 * ( (m_tx_bytes - m_tx_bytes_last) / (float)elapsed );
m_stats_rx_bytes_per_sec = (float)1000 * ( (m_rx_bytes - m_rx_bytes_last) / (float)elapsed );
m_rx_bytes_last = m_rx_bytes;
m_tx_bytes_last = m_tx_bytes;
emit statsTick( m_stats_tx_bytes_per_sec, m_stats_rx_bytes_per_sec );
}

129
src/connection.h Normal file
View File

@@ -0,0 +1,129 @@
#ifndef CONNECTION_H
#define CONNECTION_H
#include <QSharedPointer>
#include <QTcpSocket>
#include <QHostAddress>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QDebug>
#include <QDataStream>
#include <QtEndian>
#include <QTimer>
#include <QTime>
#include <QPointer>
#include <qjson/parser.h>
#include <qjson/serializer.h>
#include <qjson/qobjecthelper.h>
#include "msg.h"
#include "msgprocessor.h"
class Servent;
class Connection : public QObject
{
Q_OBJECT
public:
Connection( Servent* parent );
virtual ~Connection();
virtual Connection* clone() = 0;
QString id() const;
void setId( const QString& );
void setFirstMessage( const QVariant& m );
void setFirstMessage( msg_ptr m );
msg_ptr firstMessage() const { return m_firstmsg; };
const QPointer<QTcpSocket>& socket() { return m_sock; };
void setOutbound( bool o ) { m_outbound = o; };
bool outbound() const { return m_outbound; }
Servent* servent() { return m_servent; };
// get public port of remote peer:
int peerPort() { return m_peerport; };
void setPeerPort( int p ) { m_peerport = p; };
void markAsFailed();
void setName( const QString& n ) { m_name = n; };
QString name() const { return m_name; };
void setOnceOnly( bool b ) { m_onceonly = b; };
bool onceOnly() const { return m_onceonly; };
bool isReady() const { return m_ready; } ;
bool isRunning() const { return m_sock != 0; }
qint64 bytesSent() const { return m_tx_bytes; }
qint64 bytesReceived() const { return m_rx_bytes; }
void setMsgProcessorModeOut( quint32 m ) { m_msgprocessor_out.setMode(m); }
void setMsgProcessorModeIn( quint32 m ) { m_msgprocessor_in.setMode(m); }
signals:
void ready();
void failed();
void finished();
void statsTick( qint64 tx_bytes_sec, qint64 rx_bytes_sec );
void socketClosed();
void socketErrored(QAbstractSocket::SocketError);
protected:
virtual void setup() = 0;
protected slots:
virtual void handleMsg( msg_ptr msg ) = 0;
public slots:
virtual void start( QTcpSocket* sock );
void sendMsg( QVariant );
void sendMsg( msg_ptr );
void shutdown( bool waitUntilSentAll = false );
private slots:
void handleIncomingQueueEmpty();
void sendMsg_now( msg_ptr );
void socketDisconnected();
void socketDisconnectedError(QAbstractSocket::SocketError);
void readyRead();
void doSetup();
void authCheckTimeout();
void bytesWritten( qint64 );
void calcStats();
protected:
QPointer<QTcpSocket> m_sock;
int m_peerport;
msg_ptr m_msg;
QJson::Parser parser;
Servent* m_servent;
bool m_outbound, m_ready, m_onceonly;
msg_ptr m_firstmsg;
QString m_name;
private:
void handleReadMsg();
void actualShutdown();
bool m_do_shutdown, m_actually_shutting_down, m_peer_disconnected;
qint64 m_tx_bytes, m_tx_bytes_requested;
qint64 m_rx_bytes;
QString m_id;
QTimer* m_statstimer;
QTime m_statstimer_mark;
qint64 m_stats_tx_bytes_per_sec, m_stats_rx_bytes_per_sec;
qint64 m_rx_bytes_last, m_tx_bytes_last;
MsgProcessor m_msgprocessor_in, m_msgprocessor_out;
};
#endif // CONNECTION_H

209
src/controlconnection.cpp Normal file
View File

@@ -0,0 +1,209 @@
#include "controlconnection.h"
#include "tomahawk/tomahawkapp.h"
#include "remotecollection.h"
#include "filetransferconnection.h"
#include "database.h"
#include "databasecommand_collectionstats.h"
#include "dbsyncconnection.h"
using namespace Tomahawk;
ControlConnection::ControlConnection( Servent* parent )
: Connection( parent )
, m_dbsyncconn( 0 )
, m_registered( false )
{
qDebug() << "CTOR controlconnection";
setId("ControlConnection()");
// auto delete when connection closes:
connect( this, SIGNAL( finished() ), SLOT( deleteLater() ) );
this->setMsgProcessorModeIn( MsgProcessor::UNCOMPRESS_ALL | MsgProcessor::PARSE_JSON );
this->setMsgProcessorModeOut( MsgProcessor::COMPRESS_IF_LARGE );
}
ControlConnection::~ControlConnection()
{
qDebug() << "DTOR controlconnection";
m_servent->unregisterControlConnection(this);
if( m_dbsyncconn ) m_dbsyncconn->deleteLater();
}
Connection*
ControlConnection::clone()
{
ControlConnection * clone = new ControlConnection(servent());
clone->setOnceOnly(onceOnly());
clone->setName(name());
return clone;
}
void
ControlConnection::setup()
{
qDebug() << Q_FUNC_INFO << id() << name();
// setup source and remote collection for this peer
m_source = source_ptr( new Source( id(), this ) );
if( Servent::isIPWhitelisted( m_sock->peerAddress() ) )
{
// FIXME TODO blocking DNS lookup if LAN, slow/fails on windows?
QHostInfo i = QHostInfo::fromName( m_sock->peerAddress().toString() );
if( i.hostName().length() )
{
m_source->setFriendlyName( i.hostName() );
}
}
else
{
m_source->setFriendlyName( QString( "%1" ).arg( name() ) );
}
// delay setting up collection/etc until source is synced.
// we need it DB synced so it has an ID + exists in DB.
connect( m_source.data(), SIGNAL( syncedWithDatabase() ),
SLOT( registerSource() ), Qt::QueuedConnection );
m_source->doDBSync();
}
// source was synced to DB, set it up properly:
void
ControlConnection::registerSource()
{
qDebug() << Q_FUNC_INFO;
Source * source = (Source*) sender();
Q_ASSERT( source == m_source.data() );
// .. but we'll use the shared pointer we've already made:
collection_ptr coll( new RemoteCollection( m_source ) );
m_source->addCollection( coll );
TomahawkApp::instance()->sourcelist().add( m_source );
m_registered = true;
setupDbSyncConnection();
m_servent->registerControlConnection(this);
}
void
ControlConnection::setupDbSyncConnection( bool ondemand )
{
if( m_dbsyncconn != NULL || ! m_registered )
return;
qDebug() << Q_FUNC_INFO << ondemand << m_source->id();
Q_ASSERT( m_source->id() > 0 );
if( ! m_dbconnkey.isEmpty() )
{
qDebug() << "Connecting to DBSync offer from peer...";
m_dbsyncconn = new DBSyncConnection( m_servent, m_source );
connect( m_dbsyncconn, SIGNAL( finished() ),
m_dbsyncconn, SLOT( deleteLater() ) );
connect( m_dbsyncconn, SIGNAL( destroyed( QObject* ) ),
SLOT( dbSyncConnFinished( QObject* ) ), Qt::DirectConnection );
m_servent->createParallelConnection( this, m_dbsyncconn, m_dbconnkey );
m_dbconnkey.clear();
}
else if( !outbound() || ondemand ) // only one end makes the offer
{
qDebug() << "Offering a DBSync key to peer...";
m_dbsyncconn = new DBSyncConnection( m_servent, m_source );
connect( m_dbsyncconn, SIGNAL( finished() ),
m_dbsyncconn, SLOT( deleteLater()) );
connect( m_dbsyncconn, SIGNAL( destroyed(QObject* ) ),
SLOT( dbSyncConnFinished( QObject* ) ), Qt::DirectConnection );
QString key = uuid();
m_servent->registerOffer( key, m_dbsyncconn );
QVariantMap m;
m.insert( "method", "dbsync-offer" );
m.insert( "key", key );
sendMsg( m );
}
}
void
ControlConnection::dbSyncConnFinished( QObject* c )
{
qDebug() << Q_FUNC_INFO << "DBSync connection closed (for now)";
if( (DBSyncConnection*)c == m_dbsyncconn )
{
//qDebug() << "Setting m_dbsyncconn to NULL";
m_dbsyncconn = NULL;
}
}
DBSyncConnection*
ControlConnection::dbSyncConnection()
{
qDebug() << Q_FUNC_INFO;
if( m_dbsyncconn == NULL )
setupDbSyncConnection( true );
return m_dbsyncconn;
}
void
ControlConnection::handleMsg( msg_ptr msg )
{
// if small and not compresed, print it out for debug
if( msg->length() < 1024 && !msg->is( Msg::COMPRESSED ) )
{
qDebug() << id() << "got msg:" << QString::fromAscii( msg->payload() );
}
// All control connection msgs are JSON
if( !msg->is( Msg::JSON ) )
{
Q_ASSERT( msg->is( Msg::JSON ) );
markAsFailed();
return;
}
QVariantMap m = msg->json().toMap();
if( !m.isEmpty() )
{
if( m.value("conntype").toString() == "request-offer" )
{
QString theirkey = m["key"].toString();
QString ourkey = m["offer"].toString();
servent()->reverseOfferRequest( this, ourkey, theirkey );
}
else if( m.value( "method" ).toString() == "dbsync-offer" )
{
m_dbconnkey = m.value( "key" ).toString() ;
setupDbSyncConnection();
}
else if( m.value( "method" ) == "protovercheckfail" )
{
qDebug() << "*** Remote peer protocol version mismatch, connection closed";
shutdown( true );
return;
}
else
{
qDebug() << id() << "Unhandled msg:" << QString::fromAscii( msg->payload() );
}
return;
}
qDebug() << id() << "Invalid msg:" << QString::fromAscii(msg->payload());
}

51
src/controlconnection.h Normal file
View File

@@ -0,0 +1,51 @@
/*
One ControlConnection always remains open to each peer.
They arrange connections/reverse connections, inform us
when the peer goes offline, and own+setup DBSyncConnections.
*/
#ifndef CONTROLCONNECTION_H
#define CONTROLCONNECTION_H
#include "connection.h"
#include "servent.h"
#include "tomahawk/source.h"
#include "tomahawk/typedefs.h"
class FileTransferSession;
class ControlConnection : public Connection
{
Q_OBJECT
public:
explicit ControlConnection( Servent* parent = 0 );
~ControlConnection();
Connection* clone();
DBSyncConnection* dbSyncConnection();
protected:
virtual void setup();
protected slots:
virtual void handleMsg( msg_ptr msg );
signals:
private slots:
void dbSyncConnFinished( QObject* c );
void registerSource();
private:
void setupDbSyncConnection( bool ondemand = false );
Tomahawk::source_ptr m_source;
DBSyncConnection* m_dbsyncconn;
QString m_dbconnkey;
bool m_registered;
};
#endif // CONTROLCONNECTION_H

22
src/database/README.txt Normal file
View File

@@ -0,0 +1,22 @@
To query or modify the database you must use a DatabaseCommand.
The DatabaseCommand objects are processed sequentially and asynchronously
by the DatabaseWorker.
This means you need to dispatch the cmd, and connect to a finished signal.
There are no blocking DB calls in the application code, except in the
exec() method of a DatabaseCommand object.
If you inherit DatabaseCommandLoggable, the command is serialized into the
oplog, so that peers can replay it against their cache of your database.
For example, if you dispatch an addTracks DBCmd, after scanning a new album,
this will be serialized, and peers will replay it so that their cached version
of your collection is kept up to date.
DBCmds have GUIDs, and are ordered by the 'id' in the oplog table.
The last DBCmd GUID applied to your cache of a source's collection is stored
in the source table (the lastop field).
The DBSyncConnection will ask for all ops newer than that GUID, and replay.

50
src/database/database.cpp Normal file
View File

@@ -0,0 +1,50 @@
#include "database.h"
Database::Database( const QString& dbname, QObject* parent )
: QObject( parent )
, m_impl( new DatabaseImpl( dbname, this ) )
, m_workerRO( new DatabaseWorker( m_impl, this, false ) )
, m_workerRW( new DatabaseWorker( m_impl, this, true ) )
{
m_workerRO->start();
m_workerRW->start();
}
Database::~Database()
{
qDebug() << Q_FUNC_INFO;
delete m_workerRW;
delete m_workerRO;
delete m_impl;
}
void
Database::loadIndex()
{
m_impl->loadIndex();
}
void
Database::enqueue( QSharedPointer<DatabaseCommand> lc )
{
if( lc->doesMutates() )
{
//qDebug() << Q_FUNC_INFO << "RW" << lc->commandname();
emit newJobRO( lc );
}
else
{
//qDebug() << Q_FUNC_INFO << "RO" << lc->commandname();
emit newJobRW( lc );
}
}
const QString&
Database::dbid() const
{
return m_impl->dbid();
}

48
src/database/database.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef DATABASE_H
#define DATABASE_H
#include <QSharedPointer>
#include <QVariant>
#include "databaseimpl.h"
#include "databasecommand.h"
#include "databaseworker.h"
/*
This class is really a firewall/pimpl - the public functions of LibraryImpl
are the ones that operate on the database, without any locks.
HOWEVER, we're using the command pattern to serialize access to the database
and provide an async api. You create a DatabaseCommand object, and add it to
the queue of work. There is a single thread responsible for exec'ing all
the commands, so sqlite only does one thing at a time.
Update: 1 thread for mutates, one for readonly queries.
*/
class Database : public QObject
{
Q_OBJECT
public:
explicit Database( const QString& dbname, QObject* parent = 0 );
~Database();
const QString& dbid() const;
const bool indexReady() const { return m_indexReady; }
void loadIndex();
signals:
void indexReady(); // search index
void newJobRO( QSharedPointer<DatabaseCommand> );
void newJobRW( QSharedPointer<DatabaseCommand> );
public slots:
void enqueue( QSharedPointer<DatabaseCommand> lc );
private:
DatabaseImpl* m_impl;
DatabaseWorker *m_workerRO, *m_workerRW;
bool m_indexReady;
};
#endif // DATABASE_H

View File

@@ -0,0 +1,75 @@
#include "databasecollection.h"
#include "tomahawk/tomahawkapp.h"
#include "database.h"
#include "databasecommand_alltracks.h"
#include "databasecommand_addfiles.h"
#include "databasecommand_loadallplaylists.h"
using namespace Tomahawk;
DatabaseCollection::DatabaseCollection( const source_ptr& src, QObject* parent )
: Collection( src, QString( "dbcollection:%1" ).arg( src->userName() ), parent )
{
}
void
DatabaseCollection::loadPlaylists()
{
qDebug() << Q_FUNC_INFO;
// load our playlists
DatabaseCommand_LoadAllPlaylists* cmd = new DatabaseCommand_LoadAllPlaylists( source() );
connect( cmd, SIGNAL( done( const QList<Tomahawk::playlist_ptr>& ) ),
SLOT( setPlaylists( const QList<Tomahawk::playlist_ptr>& ) ) );
TomahawkApp::instance()->database()->enqueue(
QSharedPointer<DatabaseCommand>( cmd )
);
}
void
DatabaseCollection::loadAllTracks( boost::function<void( const QList<QVariant>&, collection_ptr )> callback )
{
qDebug() << Q_FUNC_INFO << source()->userName();
m_callback = callback;
DatabaseCommand_AllTracks* cmd = new DatabaseCommand_AllTracks( source() );
connect( cmd, SIGNAL( done( const QList<QVariant>& ) ),
SLOT( callCallback( const QList<QVariant>& ) ) );
TomahawkApp::instance()->database()->enqueue(
QSharedPointer<DatabaseCommand>( cmd )
);
}
void
DatabaseCollection::addTracks( const QList<QVariant> &newitems )
{
qDebug() << Q_FUNC_INFO << newitems.length();
DatabaseCommand_AddFiles* cmd = new DatabaseCommand_AddFiles( newitems, source() );
TomahawkApp::instance()->database()->enqueue(
QSharedPointer<DatabaseCommand>( cmd )
);
}
void
DatabaseCollection::removeTracks( const QList<QVariant> &olditems )
{
// FIXME
Q_ASSERT( false );
// TODO RemoveTracks cmd, probably builds a temp table of all the URLs in
// olditems, then joins on that to batch-delete.
}
void
DatabaseCollection::callCallback( const QList<QVariant>& res )
{
qDebug() << Q_FUNC_INFO << res.length() << this->source()->collection().data();
m_callback( res, this->source()->collection() );
}

View File

@@ -0,0 +1,31 @@
#ifndef DATABASECOLLECTION_H
#define DATABASECOLLECTION_H
#include "tomahawk/collection.h"
#include "tomahawk/typedefs.h"
class DatabaseCollection : public Tomahawk::Collection
{
Q_OBJECT
public:
explicit DatabaseCollection( const Tomahawk::source_ptr& source, QObject* parent = 0 );
~DatabaseCollection()
{
qDebug() << Q_FUNC_INFO;
}
virtual void loadAllTracks( boost::function<void( const QList<QVariant>&, Tomahawk::collection_ptr )> callback );
virtual void loadPlaylists();
public slots:
virtual void addTracks( const QList<QVariant> &newitems );
virtual void removeTracks( const QList<QVariant> &olditems );
void callCallback( const QList<QVariant>& res );
private:
boost::function<void( const QList<QVariant>&, Tomahawk::collection_ptr )> m_callback;
};
#endif // DATABASECOLLECTION_H

View File

@@ -0,0 +1,83 @@
#include "databasecommand.h"
#include <QDebug>
#include "databasecommand_addfiles.h"
#include "databasecommand_createplaylist.h"
#include "databasecommand_deleteplaylist.h"
#include "databasecommand_setplaylistrevision.h"
DatabaseCommand::DatabaseCommand( QObject* parent )
: QObject( parent )
, m_state( PENDING )
{
//qDebug() << Q_FUNC_INFO;
}
DatabaseCommand::DatabaseCommand( const source_ptr& src, QObject* parent )
: QObject( parent )
, m_state( PENDING )
, m_source( src )
{
//qDebug() << Q_FUNC_INFO;
}
DatabaseCommand::~DatabaseCommand()
{
//qDebug() << Q_FUNC_INFO;
}
void
DatabaseCommand::_exec( DatabaseImpl* lib )
{
//qDebug() << "RUNNING" << thread();
m_state = RUNNING;
emit running();
exec( lib );
m_state=FINISHED;
//qDebug() << "FINISHED" << thread();
}
DatabaseCommand*
DatabaseCommand::factory( const QVariant& op, const source_ptr& source )
{
const QString name = op.toMap().value( "command" ).toString();
if( name == "addfiles" )
{
DatabaseCommand_AddFiles * cmd = new DatabaseCommand_AddFiles;
cmd->setSource( source );
QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd );
return cmd;
}
else if( name == "createplaylist" )
{
DatabaseCommand_CreatePlaylist * cmd = new DatabaseCommand_CreatePlaylist;
cmd->setSource( source );
QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd );
return cmd;
}
else if( name == "deleteplaylist" )
{
DatabaseCommand_DeletePlaylist * cmd = new DatabaseCommand_DeletePlaylist;
cmd->setSource( source );
QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd );
return cmd;
}
else if( name == "setplaylistrevision" )
{
DatabaseCommand_SetPlaylistRevision * cmd = new DatabaseCommand_SetPlaylistRevision;
cmd->setSource( source );
QJson::QObjectHelper::qvariant2qobject( op.toMap(), cmd );
return cmd;
}
qDebug() << "ERRROR in" << Q_FUNC_INFO;
Q_ASSERT( false );
return NULL;
}

View File

@@ -0,0 +1,82 @@
#ifndef DATABASECOMMAND_H
#define DATABASECOMMAND_H
#include <QObject>
#include <QMetaType>
#include <QTime>
#include <QSqlQuery>
#include "tomahawk/source.h"
#include "tomahawk/typedefs.h"
#include "database/op.h"
class DatabaseImpl;
class DatabaseCommand : public QObject
{
Q_OBJECT
Q_PROPERTY( QString guid READ guid WRITE setGuid )
public:
enum State {
PENDING = 0,
RUNNING = 1,
FINISHED = 2
};
explicit DatabaseCommand( QObject* parent = 0 );
explicit DatabaseCommand( const Tomahawk::source_ptr& src, QObject* parent = 0 );
DatabaseCommand( const DatabaseCommand &other )
{
}
virtual ~DatabaseCommand();
virtual QString commandname() const { return "DatabaseCommand"; }
virtual bool doesMutates() const { return true; }
State state() const { return m_state; }
// if i make this pure virtual, i get compile errors in qmetatype.h.
// we need Q_DECLARE_METATYPE to use in queued sig/slot connections.
virtual void exec( DatabaseImpl* lib ) { Q_ASSERT( false ); }
void _exec( DatabaseImpl* lib );
// stuff to do once transaction applied ok.
// Don't change the database from in here, duh.
void postCommit() { postCommitHook(); emit committed(); }
virtual void postCommitHook(){};
void setSource( const Tomahawk::source_ptr& s ) { m_source = s; }
const Tomahawk::source_ptr& source() const { return m_source; }
virtual bool loggable() const { return false; }
QString guid() const
{
if( m_guid.isEmpty() )
m_guid = uuid();
return m_guid;
}
void setGuid( const QString& g ) { m_guid = g; }
void emitFinished() { emit finished(); }
static DatabaseCommand* factory( const QVariant& op, const Tomahawk::source_ptr& source );
signals:
void running();
void finished();
void committed();
private:
State m_state;
Tomahawk::source_ptr m_source;
mutable QString m_guid;
};
Q_DECLARE_METATYPE( DatabaseCommand )
#endif // DATABASECOMMAND_H

View File

@@ -0,0 +1,174 @@
#include "databasecommand_addfiles.h"
#include <QSqlQuery>
#include "tomahawk/collection.h"
#include "tomahawk/tomahawkapp.h"
#include "database.h"
#include "databasecommand_collectionstats.h"
#include "databaseimpl.h"
#include "controlconnection.h"
using namespace Tomahawk;
// remove file paths when making oplog/for network transmission
QVariantList
DatabaseCommand_AddFiles::files() const
{
QVariantList list;
foreach( const QVariant& v, m_files )
{
// replace url with the id, we don't leak file paths over the network.
QVariantMap m = v.toMap();
m.remove( "url" );
m.insert( "url", QString::number( m.value( "id" ).toInt() ) );
list.append( m );
}
return list;
}
// After changing a collection, we need to tell other bits of the system:
void
DatabaseCommand_AddFiles::postCommitHook()
{
qDebug() << Q_FUNC_INFO;
// make the collection object emit its tracksAdded signal, so the
// collection browser will update/fade in etc.
Collection* coll = source()->collection().data();
connect( this, SIGNAL( notify( const QList<QVariant>&, Tomahawk::collection_ptr ) ),
coll, SIGNAL( tracksAdded( const QList<QVariant>&, Tomahawk::collection_ptr ) ),
Qt::QueuedConnection );
// do it like this so it gets called in the right thread:
emit notify( m_files, source()->collection() );
// also re-calc the collection stats, to updates the "X tracks" in the sidebar etc:
DatabaseCommand_CollectionStats* cmd = new DatabaseCommand_CollectionStats( source() );
connect( cmd, SIGNAL( done( const QVariantMap& ) ),
source().data(), SLOT( setStats( const QVariantMap& ) ), Qt::QueuedConnection );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
if( source()->isLocal() )
APP->servent().triggerDBSync();
}
void
DatabaseCommand_AddFiles::exec( DatabaseImpl* dbi )
{
qDebug() << Q_FUNC_INFO;
Q_ASSERT( !source().isNull() );
TomahawkSqlQuery query_file = dbi->newquery();
TomahawkSqlQuery query_filejoin = dbi->newquery();
TomahawkSqlQuery query_file_del = dbi->newquery();
query_file.prepare( "INSERT INTO file(source, url, size, mtime, md5, mimetype, duration, bitrate) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)" );
query_filejoin.prepare( "INSERT INTO file_join(file, artist ,album, track, albumpos) "
"VALUES (?,?,?,?,?)" );
query_file_del.prepare( QString( "DELETE FROM file WHERE source %1 AND url = ?" )
.arg( source()->isLocal() ? "IS NULL" : QString( "= %1" ).arg( source()->id() )
) );
int maxart, maxalb, maxtrk; // store max id, so we can index new ones after
maxart = maxalb = maxtrk = 0;
int added = 0;
QVariant srcid = source()->isLocal() ?
QVariant( QVariant::Int ) : source()->id();
qDebug() << "Adding" << m_files.length() << "files to db for source" << srcid;
QList<QVariant>::iterator it;
for( it = m_files.begin(); it != m_files.end(); ++it )
{
QVariant& v = *it;
QVariantMap m = v.toMap();
QString url = m.value( "url" ).toString();
int mtime = m.value( "lastmodified" ).toInt();
int size = m.value( "size" ).toInt();
QString hash = m.value( "hash" ).toString();
QString mimetype = m.value( "mimetype" ).toString();
int duration = m.value( "duration" ).toInt();
int bitrate = m.value( "bitrate" ).toInt();
QString artist = m.value( "artist" ).toString();
QString album = m.value( "album" ).toString();
QString track = m.value( "track" ).toString();
int albumpos = m.value( "albumpos" ).toInt();
int fileid = 0;
query_file_del.bindValue( 0, url );
query_file_del.exec();
query_file.bindValue( 0, srcid );
query_file.bindValue( 1, url );
query_file.bindValue( 2, size );
query_file.bindValue( 3, mtime );
query_file.bindValue( 4, hash );
query_file.bindValue( 5, mimetype );
query_file.bindValue( 6, duration );
query_file.bindValue( 7, bitrate );
if( !query_file.exec() )
{
qDebug() << "Failed to insert to file:"
<< query_file.lastError().databaseText()
<< query_file.lastError().driverText()
<< query_file.boundValues();
continue;
}
else
{
if( added % 100 == 0 ) qDebug() << "Inserted" << added;
}
// get internal IDs for art/alb/trk
fileid = query_file.lastInsertId().toInt();
// insert the new fileid, set the url for our use:
m.insert( "id", fileid );
if( !source()->isLocal() ) m["url"] = QString( "servent://%1\t%2" )
.arg( source()->userName() )
.arg( fileid );
v = m;
bool isnew;
int artid = dbi->artistId( artist, isnew );
if( artid < 1 ) continue;
if( isnew && maxart == 0 ) maxart = artid;
int trkid = dbi->trackId( artid, track, isnew );
if( trkid < 1 ) continue;
if( isnew && maxtrk == 0 ) maxtrk = trkid;
int albid = dbi->albumId( artid, album, isnew );
if( albid > 0 && isnew && maxalb == 0 ) maxalb = albid;
// Now add the association
query_filejoin.bindValue( 0, fileid );
query_filejoin.bindValue( 1, artid );
query_filejoin.bindValue( 2, albid > 0 ? albid : QVariant( QVariant::Int ) );
query_filejoin.bindValue( 3, trkid );
query_filejoin.bindValue( 4, albumpos );
if( !query_filejoin.exec() )
{
qDebug() << "Error inserting into file_join table";
continue;
}
added++;
}
qDebug() << "Inserted" << added;
// TODO building the index could be a separate job, outside this transaction
if(maxart) dbi->updateSearchIndex( "artist", maxart );
if(maxalb) dbi->updateSearchIndex( "album", maxalb );
if(maxtrk) dbi->updateSearchIndex( "track", maxtrk );
qDebug() << "Committing" << added << "tracks...";
qDebug() << "Done.";
emit done( m_files, source()->collection() );
}

View File

@@ -0,0 +1,43 @@
#ifndef DATABASECOMMAND_ADDFILES_H
#define DATABASECOMMAND_ADDFILES_H
#include <QObject>
#include <QVariantMap>
#include "database/databasecommandloggable.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_AddFiles : public DatabaseCommandLoggable
{
Q_OBJECT
Q_PROPERTY( QVariantList files READ files WRITE setFiles )
public:
explicit DatabaseCommand_AddFiles( QObject* parent = 0 )
: DatabaseCommandLoggable( parent )
{}
explicit DatabaseCommand_AddFiles( const QList<QVariant>& files, const Tomahawk::source_ptr& source, QObject* parent = 0 )
: DatabaseCommandLoggable( parent ), m_files( files )
{
setSource( source );
}
virtual QString commandname() const { return "addfiles"; }
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return true; }
virtual void postCommitHook();
QVariantList files() const;
void setFiles( const QVariantList& f ) { m_files = f; }
signals:
void done( const QList<QVariant>&, Tomahawk::collection_ptr );
void notify( const QList<QVariant>&, Tomahawk::collection_ptr );
private:
QVariantList m_files;
};
#endif // DATABASECOMMAND_ADDFILES_H

View File

@@ -0,0 +1,45 @@
#include <QSqlQuery>
#include "databasecommand_addsource.h"
#include "databaseimpl.h"
DatabaseCommand_addSource::DatabaseCommand_addSource( const QString& username, const QString& fname, QObject* parent )
: DatabaseCommand( parent )
, m_username( username )
, m_fname( fname )
{
}
void
DatabaseCommand_addSource::exec( DatabaseImpl* dbi )
{
TomahawkSqlQuery query = dbi->newquery();
query.prepare( "SELECT id, friendlyname FROM source WHERE name = ?" );
query.addBindValue( m_username );
query.exec();
if ( query.next() )
{
unsigned int id = query.value( 0 ).toInt();
QString fname = query.value( 1 ).toString();
query.prepare( "UPDATE source SET isonline = 'true', friendlyname = ? WHERE id = ?" );
query.addBindValue( m_fname );
query.addBindValue( id );
query.exec();
emit done( id, fname );
return;
}
query.prepare( "INSERT INTO source(name, friendlyname, isonline) VALUES(?,?,?)" );
query.addBindValue( m_username );
query.addBindValue( m_fname );
query.addBindValue( true );
bool ok = query.exec();
Q_ASSERT( ok );
unsigned int id = query.lastInsertId().toUInt();
qDebug() << "Inserted new source to DB, id:" << id << " friendlyname" << m_username;
emit done( id, m_fname );
}

View File

@@ -0,0 +1,25 @@
#ifndef DATABASECOMMAND_ADDSOURCE_H
#define DATABASECOMMAND_ADDSOURCE_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
class DatabaseCommand_addSource : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_addSource( const QString& username, const QString& fname, QObject* parent = 0 );
virtual void exec( DatabaseImpl* lib );
virtual bool doesMutates() const { return true; }
virtual QString commandname() const { return "addsource"; }
signals:
void done( unsigned int, const QString& );
private:
QString m_username, m_fname;
};
#endif // DATABASECOMMAND_ADDSOURCE_H

View File

@@ -0,0 +1,58 @@
#include "databasecommand_alltracks.h"
#include <QSqlQuery>
#include "databaseimpl.h"
void
DatabaseCommand_AllTracks::exec( DatabaseImpl* dbi )
{
Q_ASSERT( !m_source.isNull() );
TomahawkSqlQuery query = dbi->newquery();
QList<QVariant> tracks;
QString sql = QString(
"SELECT file.id, artist.name, album.name, track.name, file.size, "
" file.duration, file.bitrate, file.url, file.source, file.mtime, file.mimetype, file_join.albumpos "
"FROM file, artist, track, file_join "
"LEFT OUTER JOIN album "
"ON file_join.album = album.id "
"WHERE file.id = file_join.file "
"AND file_join.artist = artist.id "
"AND file_join.track = track.id "
"AND file.source %1 "
).arg( m_source->isLocal() ? "IS NULL" : QString( "= %1" ).arg(m_source->id() ) );
//qDebug() << sql;
query.prepare( sql );
if( !query.exec() )
{
qDebug() << "ERROR: " << dbi->database().lastError().databaseText() << dbi->database().lastError().driverText();
}
while( query.next() )
{
QVariantMap t;
QString url;
url = query.value( 7 ).toString();
if( m_source->isLocal() )
t["url"] = url;
else
t["url"] = QString( "servent://%1\t%2" ).arg( m_source->userName() ).arg( url );
t["id"] = QString( "%1" ).arg( query.value( 0 ).toInt() );
t["artist"] = query.value( 1 ).toString();
t["album"] = query.value( 2 ).toString();
t["track"] = query.value( 3 ).toString();
t["size"] = query.value( 4 ).toInt();
t["duration"] = query.value( 5 ).toInt();
t["bitrate"] = query.value( 6 ).toInt();
t["lastmodified"] = query.value( 9 ).toInt();
t["mimetype"] = query.value( 10 ).toString();
t["albumpos"] = query.value( 11 ).toUInt();
tracks.append( t );
}
qDebug() << Q_FUNC_INFO << tracks.length();
emit done( tracks );
}

View File

@@ -0,0 +1,31 @@
#ifndef DATABASECOMMAND_ALLTRACKS_H
#define DATABASECOMMAND_ALLTRACKS_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/source.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_AllTracks : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_AllTracks( const Tomahawk::source_ptr& source, QObject* parent = 0 )
: DatabaseCommand( parent ), m_source( source )
{}
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "alltracks"; }
signals:
void done( const QList<QVariant>& );
private:
Tomahawk::source_ptr m_source;
};
#endif // DATABASECOMMAND_ALLTRACKS_H

View File

@@ -0,0 +1,57 @@
#include "databasecommand_collectionstats.h"
#include "databaseimpl.h"
using namespace Tomahawk;
DatabaseCommand_CollectionStats::DatabaseCommand_CollectionStats( const source_ptr& source, QObject* parent )
: DatabaseCommand( source, parent )
{
}
void
DatabaseCommand_CollectionStats::exec( DatabaseImpl* dbi )
{
//qDebug() << Q_FUNC_INFO;
Q_ASSERT( !source().isNull() );
TomahawkSqlQuery query = dbi->newquery();
Q_ASSERT( source()->isLocal() || source()->id() >= 1 );
if( source()->isLocal() )
{
query.exec("SELECT count(*), max(mtime), (SELECT guid FROM oplog WHERE source IS NULL ORDER BY id DESC LIMIT 1) "
"FROM file "
"WHERE source IS NULL");
}
else
{
query.prepare("SELECT count(*), max(mtime), "
" (SELECT lastop FROM source WHERE id = ?) "
"FROM file "
"WHERE source = ?"
);
query.addBindValue( source()->id() );
query.addBindValue( source()->id() );
}
if( !query.exec() )
{
qDebug() << "Failed to get collection stats:" << query.boundValues();
throw "failed to get collection stats";
}
QVariantMap m;
if( query.next() )
{
m.insert( "numfiles", query.value( 0 ).toInt() );
m.insert( "lastmodified", query.value( 1 ).toInt() );
m.insert( "lastop", query.value( 2 ).toString() );
}
//qDebug() << "Loaded collection stats for"
// << (source()->isLocal() ? "LOCAL" : source()->username())
// << m;
emit done( m );
}

View File

@@ -0,0 +1,24 @@
#ifndef DATABASECOMMAND_COLLECTIONSTATS_H
#define DATABASECOMMAND_COLLECTIONSTATS_H
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/source.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_CollectionStats : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_CollectionStats( const Tomahawk::source_ptr& source, QObject* parent = 0 );
virtual void exec( DatabaseImpl* lib );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "collectionstats"; }
signals:
void done( const QVariantMap& );
};
#endif // DATABASECOMMAND_COLLECTIONSTATS_H

View File

@@ -0,0 +1,71 @@
#include "databasecommand_createplaylist.h"
#include <QSqlQuery>
#include "tomahawk/tomahawkapp.h"
using namespace Tomahawk;
DatabaseCommand_CreatePlaylist::DatabaseCommand_CreatePlaylist( QObject* parent )
: DatabaseCommandLoggable( parent )
, m_report( true )
{
qDebug() << Q_FUNC_INFO << "def";
}
DatabaseCommand_CreatePlaylist::DatabaseCommand_CreatePlaylist( const source_ptr& author,
const playlist_ptr& playlist )
: DatabaseCommandLoggable( author )
, m_playlist( playlist )
, m_report( false ) //this ctor used when creating locally, reporting done elsewhere
{
qDebug() << Q_FUNC_INFO;
}
void
DatabaseCommand_CreatePlaylist::exec( DatabaseImpl* lib )
{
qDebug() << Q_FUNC_INFO;
TomahawkSqlQuery cre = lib->newquery();
cre.prepare( "INSERT INTO playlist( guid, source, shared, title, info, creator, lastmodified) "
"VALUES( :guid, :source, :shared, :title, :info, :creator, :lastmodified )" );
Q_ASSERT( !m_playlist.isNull() );
Q_ASSERT( !source().isNull() );
cre.bindValue( ":guid", m_playlist->guid() );
cre.bindValue( ":source", source()->isLocal() ? QVariant(QVariant::Int) : source()->id() );
cre.bindValue( ":shared", m_playlist->shared() );
cre.bindValue( ":title", m_playlist->title() );
cre.bindValue( ":info", m_playlist->info() );
cre.bindValue( ":creator", m_playlist->creator() );
cre.bindValue( ":lastmodified", m_playlist->lastmodified() );
qDebug() << "CREATE PLAYLIST:" << cre.boundValues();
bool ok = cre.exec();
if( !ok )
{
qDebug() << cre.lastError().databaseText()
<< cre.lastError().driverText()
<< cre.executedQuery()
<< cre.boundValues();
Q_ASSERT( ok );
}
}
void
DatabaseCommand_CreatePlaylist::postCommitHook()
{
qDebug() << Q_FUNC_INFO;
if( m_report == false )
return;
qDebug() << Q_FUNC_INFO << "..reporting..";
m_playlist->reportCreated( m_playlist );
if( source()->isLocal() )
APP->servent().triggerDBSync();
}

View File

@@ -0,0 +1,44 @@
#ifndef DATABASECOMMAND_CREATEPLAYLIST_H
#define DATABASECOMMAND_CREATEPLAYLIST_H
#include "databaseimpl.h"
#include "databasecommandloggable.h"
#include "tomahawk/playlist.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_CreatePlaylist : public DatabaseCommandLoggable
{
Q_OBJECT
Q_PROPERTY( QVariant playlist READ playlistV WRITE setPlaylistV )
public:
explicit DatabaseCommand_CreatePlaylist( QObject* parent = 0 );
explicit DatabaseCommand_CreatePlaylist( const Tomahawk::source_ptr& author, const Tomahawk::playlist_ptr& playlist );
QString commandname() const { return "createplaylist"; }
virtual void exec( DatabaseImpl* lib );
virtual void postCommitHook();
virtual bool doesMutates() const { return true; }
QVariant playlistV() const
{
return QJson::QObjectHelper::qobject2qvariant( (QObject*)m_playlist.data() );
}
void setPlaylistV( const QVariant& v )
{
qDebug() << "***********" << Q_FUNC_INFO << v;
using namespace Tomahawk;
Playlist* p = new Playlist( source() );
QJson::QObjectHelper::qvariant2qobject( v.toMap(), p );
m_playlist = playlist_ptr( p );
}
private:
Tomahawk::playlist_ptr m_playlist;
bool m_report; // call Playlist::reportCreated?
};
#endif // DATABASECOMMAND_CREATEPLAYLIST_H

View File

@@ -0,0 +1,53 @@
#include "databasecommand_deleteplaylist.h"
#include <QSqlQuery>
#include "tomahawk/tomahawkapp.h"
using namespace Tomahawk;
DatabaseCommand_DeletePlaylist::DatabaseCommand_DeletePlaylist( const source_ptr& source, const QString& playlistguid )
: DatabaseCommandLoggable( source )
{
setPlaylistguid( playlistguid );
}
void
DatabaseCommand_DeletePlaylist::exec( DatabaseImpl* lib )
{
qDebug() << Q_FUNC_INFO;
TomahawkSqlQuery cre = lib->newquery();
QString sql = QString( "DELETE FROM playlist WHERE guid = :id AND source %1" )
.arg( source()->isLocal() ? "IS NULL" : QString("= %1").arg( source()->id() ) );
cre.prepare( sql );
cre.bindValue( ":id", m_playlistguid );
bool ok = cre.exec();
if( !ok )
{
qDebug() << cre.lastError().databaseText()
<< cre.lastError().driverText()
<< cre.executedQuery()
<< cre.boundValues();
Q_ASSERT( ok );
}
}
void
DatabaseCommand_DeletePlaylist::postCommitHook()
{
qDebug() << Q_FUNC_INFO << "..reporting..";
playlist_ptr playlist = source()->collection()->playlist( m_playlistguid );
Q_ASSERT( !playlist.isNull() );
playlist->reportDeleted( playlist );
if( source()->isLocal() )
APP->servent().triggerDBSync();
}

View File

@@ -0,0 +1,34 @@
#ifndef DATABASECOMMAND_DELETEPLAYLIST_H
#define DATABASECOMMAND_DELETEPLAYLIST_H
#include "databaseimpl.h"
#include "databasecommandloggable.h"
#include "tomahawk/source.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_DeletePlaylist : public DatabaseCommandLoggable
{
Q_OBJECT
Q_PROPERTY( QString playlistguid READ playlistguid WRITE setPlaylistguid )
public:
explicit DatabaseCommand_DeletePlaylist( QObject* parent = 0 )
: DatabaseCommandLoggable( parent )
{}
explicit DatabaseCommand_DeletePlaylist( const Tomahawk::source_ptr& source, const QString& playlistguid );
QString commandname() const { return "deleteplaylist"; }
virtual void exec( DatabaseImpl* lib );
virtual void postCommitHook();
virtual bool doesMutates() const { return true; }
QString playlistguid() const { return m_playlistguid; }
void setPlaylistguid( const QString& s ) { m_playlistguid = s; }
private:
QString m_playlistguid;
};
#endif // DATABASECOMMAND_DELETEPLAYLIST_H

View File

@@ -0,0 +1,57 @@
#include "databasecommand_dirmtimes.h"
#include <QSqlQuery>
#include "databaseimpl.h"
void
DatabaseCommand_DirMtimes::exec( DatabaseImpl* dbi )
{
if( m_update )
execUpdate( dbi );
else
execSelect( dbi );
}
void
DatabaseCommand_DirMtimes::execSelect( DatabaseImpl* dbi )
{
QMap<QString,unsigned int> mtimes;
TomahawkSqlQuery query = dbi->newquery();
if( m_prefix.isEmpty() )
query.exec( "SELECT name, mtime FROM dirs_scanned" );
else
{
query.prepare( QString( "SELECT name, mtime "
"FROM dirs_scanned "
"WHERE name LIKE '%1%'" ).arg(m_prefix.replace( '\'',"''" ) ) );
query.exec();
}
while( query.next() )
{
mtimes.insert( query.value( 0 ).toString(), query.value( 1 ).toUInt() );
}
emit done( mtimes );
}
void
DatabaseCommand_DirMtimes::execUpdate( DatabaseImpl* dbi )
{
qDebug() << "Saving mtimes...";
TomahawkSqlQuery query = dbi->newquery();
query.exec( "DELETE FROM dirs_scanned" );
query.prepare( "INSERT INTO dirs_scanned(name, mtime) VALUES(?,?)" );
foreach( const QString& k, m_tosave.keys() )
{
query.bindValue( 0, k );
query.bindValue( 1, m_tosave.value( k ) );
query.exec();
}
qDebug() << "Saved mtimes for" << m_tosave.size() << "dirs.";
}

View File

@@ -0,0 +1,42 @@
#ifndef DATABASECOMMAND_DIRMTIMES_H
#define DATABASECOMMAND_DIRMTIMES_H
#include <QObject>
#include <QVariantMap>
#include <QMap>
#include "databasecommand.h"
// Not loggable, mtimes only used to speed up our local scanner.
class DatabaseCommand_DirMtimes : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_DirMtimes( const QString& prefix = "", QObject* parent = 0 )
: DatabaseCommand( parent ), m_prefix( prefix ), m_update( false )
{}
explicit DatabaseCommand_DirMtimes( QMap<QString, unsigned int> tosave, QObject* parent = 0 )
: DatabaseCommand( parent ), m_update( true ), m_tosave( tosave )
{}
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return m_update; }
virtual QString commandname() const { return "dirmtimes"; }
signals:
void done( const QMap<QString, unsigned int>& );
public slots:
private:
void execSelect( DatabaseImpl* dbi );
void execUpdate( DatabaseImpl* dbi );
QString m_prefix;
bool m_update;
QMap<QString, unsigned int> m_tosave;
};
#endif // DATABASECOMMAND_DIRMTIMES_H

View File

@@ -0,0 +1,40 @@
#include "databasecommand_importplaylist.h"
#include <QSqlQuery>
#include "tomahawk/query.h"
#include "tomahawk/playlist.h"
#include "databaseimpl.h"
void DatabaseCommand_ImportPlaylist::exec(DatabaseImpl * dbi)
{
/*
qDebug() << "Importing playlist of" << m_playlist->length() << "tracks";
TomahawkSqlQuery query = dbi->newquery();
query.prepare("INSERT INTO playlist(title, info, creator, lastmodified) "
"VALUES(?,?,?,?)");
query.addBindValue(m_playlist->title());
query.addBindValue(m_playlist->info());
query.addBindValue(m_playlist->creator());
query.addBindValue(m_playlist->lastmodified());
query.exec();
int pid = query.lastInsertId().toInt();
int pos = 0;
query.prepare("INSERT INTO playlist_tracks( "
"playlist, position, trackname, albumname, artistname) "
"VALUES (?,?,?,?,?)");
for(int k = 0; k < m_playlist->length(); k++)
{
pos++;
query.addBindValue(pid);
query.addBindValue(pos);
query.addBindValue(m_playlist->at(k)->artist());
query.addBindValue(m_playlist->at(k)->album());
query.addBindValue(m_playlist->at(k)->track());
query.exec();
}
emit done(pid);
*/
}

View File

@@ -0,0 +1,29 @@
#ifndef DATABASECOMMAND_IMPORTPLAYLIST_H
#define DATABASECOMMAND_IMPORTPLAYLIST_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/source.h"
class Playlist;
class DatabaseCommand_ImportPlaylist : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_ImportPlaylist(Playlist * p, QObject *parent = 0)
: DatabaseCommand(parent), m_playlist(p)
{}
virtual void exec(DatabaseImpl *);
virtual bool doesMutates() const { return true; }
virtual QString commandname() const { return "importplaylist"; }
signals:
void done(int id);
private:
Playlist * m_playlist;
};
#endif // DATABASECOMMAND_ADDFILES_H

View File

@@ -0,0 +1,38 @@
#include "databasecommand_loadallplaylists.h"
#include <QSqlQuery>
#include "tomahawk/playlist.h"
#include "databaseimpl.h"
using namespace Tomahawk;
void DatabaseCommand_LoadAllPlaylists::exec( DatabaseImpl* dbi )
{
TomahawkSqlQuery query = dbi->newquery();
query.exec( QString( "SELECT guid, title, info, creator, lastmodified, shared, currentrevision "
"FROM playlist WHERE source %1" )
.arg( source()->isLocal() ? "IS NULL" :
QString( "=%1" ).arg( source()->id() )
) );
QList<playlist_ptr> plists;
while ( query.next() )
{
playlist_ptr p( new Playlist( source(), //src
query.value(6).toString(), //current rev
query.value(1).toString(), //title
query.value(2).toString(), //info
query.value(3).toString(), //creator
query.value(5).toBool(), //shared
query.value(4).toInt(), //lastmod
query.value(0).toString() //GUID
) );
plists.append( p );
}
emit done( plists );
}

View File

@@ -0,0 +1,27 @@
#ifndef DATABASECOMMAND_IMPORTALLPLAYLIST_H
#define DATABASECOMMAND_IMPORTALLPLAYLIST_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/typedefs.h"
class DatabaseCommand_LoadAllPlaylists : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_LoadAllPlaylists( const Tomahawk::source_ptr& s, QObject* parent = 0 )
: DatabaseCommand( s, parent )
{}
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "loadallplaylists"; }
signals:
void done( const QList<Tomahawk::playlist_ptr>& playlists );
};
#endif // DATABASECOMMAND_ADDFILES_H

View File

@@ -0,0 +1,29 @@
#include "databasecommand_loadfile.h"
#include "databaseimpl.h"
DatabaseCommand_LoadFile::DatabaseCommand_LoadFile( const QString& id, QObject* parent )
: DatabaseCommand( parent )
, m_id( id )
{
}
void
DatabaseCommand_LoadFile::exec(DatabaseImpl* dbi)
{
QVariantMap r;
// file ids internally are really ints, at least for now:
bool ok;
do
{
unsigned int fid = m_id.toInt( &ok );
if( !ok )
break;
r = dbi->file( fid );
} while( false );
emit result( r );
}

View File

@@ -0,0 +1,27 @@
#ifndef DATABASECOMMAND_LOADFILE_H
#define DATABASECOMMAND_LOADFILE_H
#include <QObject>
#include <QVariantMap>
#include <QMap>
#include "databasecommand.h"
class DatabaseCommand_LoadFile : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_LoadFile( const QString& id, QObject* parent = 0 );
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "loadfile"; }
signals:
void result( QVariantMap );
private:
QString m_id;
};
#endif // DATABASECOMMAND_LOADFILE_H

View File

@@ -0,0 +1,37 @@
#include "databasecommand_loadops.h"
void
DatabaseCommand_loadOps::exec( DatabaseImpl* dbi )
{
QList< dbop_ptr > ops;
TomahawkSqlQuery query = dbi->newquery();
query.prepare( QString(
"SELECT guid, command, json, compressed "
"FROM oplog "
"WHERE source %1 "
"AND id > coalesce((SELECT id FROM oplog WHERE guid = ?),0) "
"ORDER BY id ASC"
).arg( source()->isLocal() ? "IS NULL" : QString("= %1").arg(source()->id()) )
);
query.addBindValue( m_since );
if( !query.exec() )
{
Q_ASSERT(0);
}
while( query.next() )
{
dbop_ptr op( new DBOp );
op->guid = query.value( 0 ).toString();
op->command = query.value( 1 ).toString();
op->payload = query.value( 2 ).toByteArray();
op->compressed = query.value( 3 ).toBool();
ops << op;
}
qDebug() << "Loaded" << ops.length() << "ops from db";
emit done( m_since, ops );
}

View File

@@ -0,0 +1,28 @@
#ifndef DATABASECOMMAND_LOADOPS_H
#define DATABASECOMMAND_LOADOPS_H
#include "tomahawk/typedefs.h"
#include "databasecommand.h"
#include "databaseimpl.h"
#include "op.h"
class DatabaseCommand_loadOps : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_loadOps( const Tomahawk::source_ptr& src, QString since, QObject* parent = 0 )
: DatabaseCommand( src ), m_since( since )
{}
virtual void exec( DatabaseImpl* db );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "loadops"; }
signals:
void done( QString lastguid, QList< dbop_ptr > ops );
private:
QString m_since; // guid to load from
};
#endif // DATABASECOMMAND_LOADOPS_H

View File

@@ -0,0 +1,106 @@
#include "databasecommand_loadplaylistentries.h"
#include <QSqlQuery>
#include "databaseimpl.h"
using namespace Tomahawk;
void
DatabaseCommand_LoadPlaylistEntries::exec( DatabaseImpl* dbi )
{
qDebug() << "Loading playlist entries for revision" << m_guid;
TomahawkSqlQuery query_entries = dbi->newquery();
query_entries.prepare("SELECT entries, playlist, author, timestamp, previous_revision "
"FROM playlist_revision "
"WHERE guid = :guid");
query_entries.bindValue( ":guid", m_guid );
bool aok = query_entries.exec();
Q_ASSERT( aok );
QStringList guids;
QMap< QString, plentry_ptr > entrymap;
bool islatest = true;
QStringList oldentries;
QString prevrev;
QJson::Parser parser; bool ok;
if( query_entries.next() )
{
// entries should be a list of strings:
QVariant v = parser.parse( query_entries.value(0).toByteArray(), &ok );
Q_ASSERT( ok && v.type() == QVariant::List ); //TODO
guids = v.toStringList();
// qDebug() << "Entries:" << guids;
QString inclause = QString("('%1')").arg(guids.join("', '"));
TomahawkSqlQuery query = dbi->newquery();
QString sql = QString("SELECT guid, trackname, artistname, albumname, annotation, "
"duration, addedon, addedby, result_hint "
"FROM playlist_item "
"WHERE guid IN %1").arg( inclause );
//qDebug() << sql;
bool xok = query.exec( sql );
Q_ASSERT( xok );
while( query.next() )
{
plentry_ptr e( new PlaylistEntry );
e->setGuid( query.value( 0 ).toString() );
e->setAnnotation( query.value( 4 ).toString() );
e->setDuration( query.value( 5 ).toUInt() );
e->setLastmodified( 0 ); // TODO e->lastmodified = query.value(6).toInt();
e->setResulthint( query.value( 8 ).toString() );
QVariantMap m;
m.insert( "artist", query.value( 2 ).toString() );
m.insert( "album", query.value( 3 ).toString() );
m.insert( "track", query.value( 1 ).toString() );
m.insert( "qid", uuid() );
Tomahawk::query_ptr q( new Tomahawk::Query( m ) );
e->setQuery( q );
entrymap.insert( e->guid(), e );
}
prevrev = query_entries.value( 4 ).toString();
}
else
{
qDebug() << "Playlist has no current revision data";
}
if( prevrev.length() )
{
TomahawkSqlQuery query_entries_old = dbi->newquery();
query_entries_old.prepare( "SELECT entries, "
"(SELECT currentrevision = ? FROM playlist WHERE guid = ?) "
"FROM playlist_revision "
"WHERE guid = ?" );
query_entries_old.addBindValue( m_guid );
query_entries_old.addBindValue( query_entries.value( 1 ).toString() );
query_entries_old.addBindValue( prevrev );
bool ex = query_entries_old.exec();
Q_ASSERT( ex );
if( !query_entries_old.next() )
{
Q_ASSERT( false );
}
QVariant v = parser.parse( query_entries_old.value( 0 ).toByteArray(), &ok );
Q_ASSERT( ok && v.type() == QVariant::List ); //TODO
oldentries = v.toStringList();
islatest = query_entries_old.value( 1 ).toBool();
}
qDebug() << Q_FUNC_INFO << "entrymap:" << entrymap;
emit done( m_guid, guids, oldentries, islatest, entrymap, true );
}

View File

@@ -0,0 +1,35 @@
#ifndef DATABASECOMMAND_LOADPLAYLIST_H
#define DATABASECOMMAND_LOADPLAYLIST_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/playlist.h"
class DatabaseCommand_LoadPlaylistEntries : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_LoadPlaylistEntries( QString revision_guid, QObject* parent = 0 )
: DatabaseCommand( parent ), m_guid( revision_guid )
{}
virtual void exec( DatabaseImpl* );
virtual bool doesMutates() const { return false; }
virtual QString commandname() const { return "loadplaylistentries"; }
signals:
void done( const QString& rev,
const QList<QString>& orderedguid,
const QList<QString>& oldorderedguid,
bool islatest,
const QMap< QString, Tomahawk::plentry_ptr >& added,
bool applied );
private:
QString m_guid;
};
#endif

View File

@@ -0,0 +1,17 @@
#include "databasecommand_modifyplaylist.h"
using namespace Tomahawk;
DatabaseCommand_ModifyPlaylist::DatabaseCommand_ModifyPlaylist( Playlist* playlist, QList< plentry_ptr > entries, Mode mode )
: DatabaseCommand()
, m_playlist( playlist )
, m_entries( entries )
, m_mode( mode )
{
}
void DatabaseCommand_ModifyPlaylist::exec( DatabaseImpl* lib )
{
}

View File

@@ -0,0 +1,39 @@
#ifndef DATABASECOMMAND_MODIFYPLAYLIST_H
#define DATABASECOMMAND_MODIFYPLAYLIST_H
#include <QObject>
#include <QVariantMap>
#include "databasecommand.h"
#include "tomahawk/source.h"
#include "tomahawk/playlist.h"
class DatabaseCommand_ModifyPlaylist : public DatabaseCommand
{
Q_OBJECT
Q_PROPERTY( int mode READ mode WRITE setMode )
public:
enum Mode
{
ADD = 1,
REMOVE = 2,
UPDATE = 3
};
explicit DatabaseCommand_ModifyPlaylist( Tomahawk::Playlist* playlist, QList< Tomahawk::plentry_ptr > entries, Mode mode );
virtual bool doesMutates() const { return true; }
virtual void exec( DatabaseImpl* lib );
int mode() const { return m_mode; }
void setMode( int m ) { m_mode = (Mode)m; }
private:
Tomahawk::Playlist* m_playlist;
QList< Tomahawk::plentry_ptr > m_entries;
Mode m_mode;
};
#endif // DATABASECOMMAND_MODIFYPLAYLIST_H

View File

@@ -0,0 +1,202 @@
#include "databasecommand_resolve.h"
#include "tomahawk/tomahawkapp.h"
#define MINSCORE 0.5
using namespace Tomahawk;
DatabaseCommand_Resolve::DatabaseCommand_Resolve( QVariant v, bool searchlocal )
: DatabaseCommand()
, m_v( v )
, m_searchlocal( searchlocal )
{
}
void
DatabaseCommand_Resolve::exec( DatabaseImpl* lib )
{
QTime timer;
const Tomahawk::QID qid = m_v.toMap().value("qid").toString();
const QString artistname = m_v.toMap().value("artist").toString();
const QString albumname = m_v.toMap().value("album").toString();
const QString trackname = m_v.toMap().value("track").toString();
//qDebug() << Q_FUNC_INFO << artistname << trackname;
/*
Resolving is a 2 stage process.
1) find list of trk/art/alb IDs that are reasonable matches to the metadata given
2) find files in database by permitted sources and calculate score, ignoring
results that are less than MINSCORE
*/
typedef QPair<int,float> scorepair_t;
// STEP 1
timer.start();
QList< int > artists = lib->searchTable( "artist", artistname, 10 );
QList< int > tracks = lib->searchTable( "track", trackname, 10 );
QList< int > albums = lib->searchTable( "album", albumname, 10 );
//qDebug() << "art" << artists.size() << "trk" << tracks.size();
//qDebug() << "searchTable calls duration:" << timer.elapsed();
if( artists.length() == 0 || tracks.length() == 0 )
{
//qDebug() << "No candidates found in first pass, aborting resolve" << artistname << trackname;
return;
}
// STEP 2
TomahawkSqlQuery files_query = lib->newquery();
QStringList artsl, trksl;
foreach( int i, artists ) artsl.append( QString::number(i) );
foreach( int i, tracks ) trksl.append( QString::number(i) );
QString sql = QString("SELECT "
"url, mtime, size, md5, mimetype, duration, bitrate, file_join.artist, file_join.album, file_join.track, "
"artist.name as artname, "
"album.name as albname, "
"track.name as trkname, "
"file.source, "
"file_join.albumpos "
"FROM file, file_join, artist, track "
"LEFT JOIN album ON album.id = file_join.album "
"WHERE "
"artist.id = file_join.artist AND "
"track.id = file_join.track AND "
"file.source %1 AND "
"file.id = file_join.file AND "
"file_join.artist IN (%2) AND "
"file_join.track IN (%3) "
"ORDER by file_join.artist,file_join.track"
).arg( m_searchlocal ? "IS NULL" : " IN (SELECT id FROM source WHERE isonline = 'true') " )
.arg( artsl.join(",") )
.arg( trksl.join(",") );
timer.start();
files_query.prepare( sql );
bool ok = files_query.exec();
Q_ASSERT( ok );
if(!ok) throw "Error";
//qDebug() << "SQL exec() duration, ms, " << timer.elapsed()
// << "numresults" << files_query.numRowsAffected();
//qDebug() << sql;
QList<Tomahawk::result_ptr> res;
while( files_query.next() )
{
QVariantMap m;
m["mtime"] = files_query.value(1).toString();
m["size"] = files_query.value(2).toInt();
m["hash"] = files_query.value(3).toString();
m["mimetype"] = files_query.value(4).toString();
m["duration"] = files_query.value(5).toInt();
m["bitrate"] = files_query.value(6).toInt();
m["artist"] = files_query.value(10).toString();
m["album"] = files_query.value(11).toString();
m["track"] = files_query.value(12).toString();
m["srcid"] = files_query.value(13).toInt();
m["albumpos"] = files_query.value(14).toUInt();
m["sid"] = uuid();
collection_ptr coll;
const QString url_str = files_query.value( 0 ).toString();
if( m_searchlocal )
{
coll = APP->sourcelist().getLocal()->collection();
m["url"] = url_str;
m["source"] = "Local Database"; // TODO
}
else
{
source_ptr s = APP->sourcelist().lookup( files_query.value( 13 ).toUInt() );
if( s.isNull() )
{
//qDebug() << "Skipping result for offline sourceid:" << files_query.value(13).toUInt();
// will happen for valid sources which are offline (and thus not in the sourcelist)
return;
}
coll = s->collection();
m.insert( "url", QString( "servent://%1\t%2" )
.arg( s->userName() )
.arg( url_str ) );
m.insert( "source", s->friendlyName() );
}
//int artid = files_query.value( 7 ).toInt();
//int albid = files_query.value( 8 ).toInt();
//int trkid = files_query.value( 9 ).toInt();
timer.start();
float score = how_similar( m_v.toMap(), m );
//qDebug() << "Score calc:" << timer.elapsed();
m["score"] = score;
//qDebug() << "RESULT" << score << m;
if( score < MINSCORE ) continue;
res << Tomahawk::result_ptr( new Tomahawk::Result( m, coll ) );
}
// return results, if any found
if( res.length() > 0 )
{
emit results( qid, res );
}
}
// TODO make clever (ft. featuring live (stuff) etc)
float
DatabaseCommand_Resolve::how_similar( const QVariantMap& q, const QVariantMap& r )
{
// query values
const QString qArtistname = DatabaseImpl::sortname( q.value("artist").toString() );
const QString qAlbumname = DatabaseImpl::sortname( q.value("album").toString() );
const QString qTrackname = DatabaseImpl::sortname( q.value("track").toString() );
// result values
const QString rArtistname = DatabaseImpl::sortname( r.value("artist").toString() );
const QString rAlbumname = DatabaseImpl::sortname( r.value("album").toString() );
const QString rTrackname = DatabaseImpl::sortname( r.value("track").toString() );
// normal edit distance
int artdist = levenshtein( qArtistname, rArtistname );
int albdist = levenshtein( qAlbumname, rAlbumname );
int trkdist = levenshtein( qTrackname, rTrackname );
// max length of name
int mlart = qMax( qArtistname.length(), rArtistname.length() );
int mlalb = qMax( qAlbumname.length(), rAlbumname.length() );
int mltrk = qMax( qTrackname.length(), rTrackname.length() );
// distance scores
float dcart = (float)( mlart - artdist ) / mlart;
float dcalb = (float)( mlalb - albdist ) / mlalb;
float dctrk = (float)( mltrk - trkdist ) / mltrk;
// don't penalize for missing album name
if( qAlbumname.length() == 0 ) dcalb = 1.0;
// weighted, so album match is worth less than track title
float combined = ( dcart*4 + dcalb + dctrk*5 ) / 10;
return combined;
}

View File

@@ -0,0 +1,103 @@
#ifndef DATABASECOMMAND_RESOLVE_H
#define DATABASECOMMAND_RESOLVE_H
#include "databasecommand.h"
#include "databaseimpl.h"
#include "tomahawk/result.h"
#include <QVariant>
class DatabaseCommand_Resolve : public DatabaseCommand
{
Q_OBJECT
public:
//explicit DatabaseCommand_Resolve(QObject *parent = 0);
explicit DatabaseCommand_Resolve( QVariant v, bool searchlocal );
virtual QString commandname() const { return "dbresolve"; }
virtual bool doesMutates() const { return false; }
virtual void exec(DatabaseImpl *lib);
signals:
void results( Tomahawk::QID qid, QList<Tomahawk::result_ptr> results );
public slots:
private:
QVariant m_v;
bool m_searchlocal;
float how_similar( const QVariantMap& q, const QVariantMap& r );
static int levenshtein(const QString& source, const QString& target)
{
// Step 1
const int n = source.length();
const int m = target.length();
if (n == 0) {
return m;
}
if (m == 0) {
return n;
}
// Good form to declare a TYPEDEF
typedef QVector< QVector<int> > Tmatrix;
Tmatrix matrix;
matrix.resize( n+1 );
// Size the vectors in the 2.nd dimension. Unfortunately C++ doesn't
// allow for allocation on declaration of 2.nd dimension of vec of vec
for (int i = 0; i <= n; i++) {
QVector<int> tmp;
tmp.resize( m+1 );
matrix.insert( i, tmp );
}
// Step 2
for (int i = 0; i <= n; i++) {
matrix[i][0]=i;
}
for (int j = 0; j <= m; j++) {
matrix[0][j]=j;
}
// Step 3
for (int i = 1; i <= n; i++) {
const QChar s_i = source[i-1];
// Step 4
for (int j = 1; j <= m; j++) {
const QChar t_j = target[j-1];
// Step 5
int cost;
if (s_i == t_j) {
cost = 0;
}
else {
cost = 1;
}
// Step 6
const int above = matrix[i-1][j];
const int left = matrix[i][j-1];
const int diag = matrix[i-1][j-1];
//int cell = min( above + 1, min(left + 1, diag + cost));
int cell = (((left+1)>(diag+cost))?diag+cost:left+1);
if(above+1 < cell) cell = above+1;
// Step 6A: Cover transposition, in addition to deletion,
// insertion and substitution. This step is taken from:
// Berghel, Hal ; Roach, David : "An Extension of Ukkonen's
// Enhanced Dynamic Programming ASM Algorithm"
// (http://www.acm.org/~hlb/publications/asm/asm.html)
if (i>2 && j>2) {
int trans=matrix[i-2][j-2]+1;
if (source[i-2]!=t_j) trans++;
if (s_i!=target[j-2]) trans++;
if (cell>trans) cell=trans;
}
matrix[i][j]=cell;
}
}
// Step 7
return matrix[n][m];
};
};
#endif // DATABASECOMMAND_RESOLVE_H

View File

@@ -0,0 +1,183 @@
#include "databasecommand_setplaylistrevision.h"
#include <QSqlQuery>
#include "tomahawksqlquery.h"
#include "tomahawk/tomahawkapp.h"
DatabaseCommand_SetPlaylistRevision::DatabaseCommand_SetPlaylistRevision(
const source_ptr& s,
QString playlistguid,
QString newrev,
QString oldrev,
QStringList orderedguids,
QList<plentry_ptr> addedentries )
: DatabaseCommandLoggable( s )
, m_newrev( newrev )
, m_oldrev( oldrev )
, m_addedentries( addedentries )
, m_applied( false )
{
setPlaylistguid( playlistguid );
QVariantList tmp;
foreach( const QString& s, orderedguids )
tmp << s;
setOrderedguids( tmp );
}
void
DatabaseCommand_SetPlaylistRevision::postCommitHook()
{
qDebug() << Q_FUNC_INFO;
QStringList orderedentriesguids;
foreach( const QVariant& v, m_orderedguids )
orderedentriesguids << v.toString();
// private, but we are a friend. will recall itself in its own thread:
playlist_ptr playlist = source()->collection()->playlist( m_playlistguid );
if ( playlist.isNull() )
{
qDebug() << m_playlistguid;
Q_ASSERT( !playlist.isNull() );
return;
}
playlist->setRevision( m_newrev,
orderedentriesguids,
m_previous_rev_orderedguids,
true, // this *is* the newest revision so far
m_addedmap,
m_applied );
if( source()->isLocal() )
APP->servent().triggerDBSync();
}
void
DatabaseCommand_SetPlaylistRevision::exec( DatabaseImpl* lib )
{
using namespace Tomahawk;
QString currentrevision;
// get the current revision for this playlist
// this also serves to check the playlist exists.
TomahawkSqlQuery chkq = lib->newquery();
chkq.prepare("SELECT currentrevision FROM playlist WHERE guid = ?");
chkq.addBindValue( m_playlistguid );
if( chkq.exec() && chkq.next() )
{
currentrevision = chkq.value( 0 ).toString();
//qDebug() << Q_FUNC_INFO << "pl guid" << m_playlistguid << " curr rev" << currentrevision;
}
else
{
throw "No such playlist, WTF?";
return;
}
QVariantList vlist = m_orderedguids;
QJson::Serializer ser;
const QByteArray entries = ser.serialize( vlist );
// add any new items:
TomahawkSqlQuery adde = lib->newquery();
QString sql = "INSERT INTO playlist_item( guid, playlist, trackname, artistname, albumname, "
"annotation, duration, addedon, addedby, result_hint ) "
"VALUES( ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )";
adde.prepare( sql );
qDebug() << "Num new playlist_items to add:" << m_addedentries.length();
foreach( const plentry_ptr& e, m_addedentries )
{
m_addedmap.insert( e->guid(), e ); // needed in postcommithook
adde.bindValue( 0, e->guid() );
adde.bindValue( 1, m_playlistguid );
adde.bindValue( 2, e->query()->track() );
adde.bindValue( 3, e->query()->artist() );
adde.bindValue( 4, e->query()->album() );
adde.bindValue( 5, e->annotation() );
adde.bindValue( 6, (int) e->duration() );
adde.bindValue( 7, e->lastmodified() );
adde.bindValue( 8, source()->isLocal() ? QVariant(QVariant::Int) : source()->id() );
adde.bindValue( 9, "" );
bool ok = adde.exec();
if( !ok )
{
qDebug() << adde.lastError().databaseText() << adde.lastError().driverText() << "\n"
<< sql << endl
<< adde.boundValues().size() ;
int i = 0;
foreach(QVariant param, adde.boundValues()) qDebug() << i++ << param;
Q_ASSERT( ok );
}
}
// add the new revision:
//qDebug() << "Adding new playlist revision, guid:" << m_newrev
// << entries;
TomahawkSqlQuery query = lib->newquery();
sql = "INSERT INTO playlist_revision(guid, playlist, entries, author, timestamp, previous_revision) "
"VALUES(?, ?, ?, ?, ?, ?)";
query.prepare( sql );
query.addBindValue( m_newrev );
query.addBindValue( m_playlistguid );
query.addBindValue( entries );
query.addBindValue( source()->isLocal() ? QVariant(QVariant::Int) : source()->id() );
query.addBindValue( 0 ); //ts
query.addBindValue( m_oldrev.isEmpty() ? QVariant(QVariant::String) : m_oldrev );
//qDebug() << sql << "\n" << query.boundValues();
bool ok = query.exec();
Q_ASSERT( ok );
qDebug() << "Currentrevision:" << currentrevision << "oldrev:" << m_oldrev;
// if optimistic locking is ok, update current revision to this new one
if( currentrevision == m_oldrev )
{
TomahawkSqlQuery query2 = lib->newquery();
qDebug() << "updating current revision, optimistic locking ok";
query2.prepare("UPDATE playlist SET currentrevision = ? WHERE guid = ?");
query2.bindValue( 0, m_newrev );
query2.bindValue( 1, m_playlistguid );
bool uok = query2.exec();
Q_ASSERT( uok );
m_applied = true;
// load previous revision entries, which we need to pass on
// so the change can be diffed
TomahawkSqlQuery query_entries = lib->newquery();
query_entries.prepare("SELECT entries, playlist, author, timestamp, previous_revision "
"FROM playlist_revision "
"WHERE guid = :guid");
query_entries.bindValue( ":guid", m_oldrev );
query_entries.exec();
if( query_entries.next() )
{
// entries should be a list of strings:
QJson::Parser parser;
QVariant v = parser.parse( query_entries.value(0).toByteArray(), &ok );
Q_ASSERT( ok && v.type() == QVariant::List ); //TODO
m_previous_rev_orderedguids = v.toStringList();
}
}
else
{
qDebug() << "Not updating current revision, optimistic locking fail";
}
}

View File

@@ -0,0 +1,82 @@
#ifndef DATABASECOMMAND_SETPLAYLISTREVISION_H
#define DATABASECOMMAND_SETPLAYLISTREVISION_H
#include "databasecommandloggable.h"
#include "databaseimpl.h"
#include "tomahawk/collection.h"
#include "tomahawk/playlist.h"
using namespace Tomahawk;
class DatabaseCommand_SetPlaylistRevision : public DatabaseCommandLoggable
{
Q_OBJECT
Q_PROPERTY( QString playlistguid READ playlistguid WRITE setPlaylistguid )
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 )
public:
explicit DatabaseCommand_SetPlaylistRevision( QObject* parent = 0 )
: DatabaseCommandLoggable( parent )
, m_applied( false )
{}
explicit DatabaseCommand_SetPlaylistRevision( const source_ptr& s,
QString playlistguid,
QString newrev,
QString oldrev,
QStringList orderedguids,
QList<Tomahawk::plentry_ptr> addedentries );
QString commandname() const { return "setplaylistrevision"; }
virtual void exec( DatabaseImpl* lib );
virtual void postCommitHook();
virtual bool doesMutates() const { return true; }
void setAddedentriesV( const QVariantList& vlist )
{
m_addedentries.clear();
foreach( const QVariant& v, vlist )
{
PlaylistEntry * pep = new PlaylistEntry;
QJson::QObjectHelper::qvariant2qobject( v.toMap(), pep );
m_addedentries << plentry_ptr(pep);
}
}
QVariantList addedentriesV() const
{
QVariantList vlist;
foreach( const plentry_ptr& pe, m_addedentries )
{
QVariant v = QJson::QObjectHelper::qobject2qvariant( pe.data() );
vlist << v;
}
return vlist;
}
void setPlaylistguid( const QString& s ) { m_playlistguid = s; }
void setNewrev( const QString& s ) { m_newrev = s; }
void setOldrev( const QString& s ) { m_oldrev = s; }
QString newrev() const { return m_newrev; }
QString oldrev() const { return m_oldrev; }
QString playlistguid() const { return m_playlistguid; }
void setOrderedguids( const QVariantList& l ) { m_orderedguids = l; }
QVariantList orderedguids() const { return m_orderedguids; }
private:
QString m_playlistguid;
QString m_newrev, m_oldrev;
QVariantList m_orderedguids;
QStringList m_previous_rev_orderedguids;
QList<Tomahawk::plentry_ptr> m_addedentries;
bool m_applied;
QMap<QString, Tomahawk::plentry_ptr> m_addedmap;
};
#endif // DATABASECOMMAND_SETPLAYLISTREVISION_H

View File

@@ -0,0 +1,16 @@
#include "databasecommand_sourceoffline.h"
DatabaseCommand_SourceOffline::DatabaseCommand_SourceOffline( int id )
: DatabaseCommand()
, m_id( id )
{
}
void DatabaseCommand_SourceOffline::exec( DatabaseImpl* lib )
{
TomahawkSqlQuery q = lib->newquery();
q.exec( QString( "UPDATE source SET isonline = 'false' WHERE id = %1" )
.arg( m_id ) );
}

View File

@@ -0,0 +1,20 @@
#ifndef DATABASECOMMAND_SOURCEOFFLINE_H
#define DATABASECOMMAND_SOURCEOFFLINE_H
#include "databasecommand.h"
#include "databaseimpl.h"
class DatabaseCommand_SourceOffline : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_SourceOffline( int id );
bool doesMutates() const { return true; }
void exec( DatabaseImpl* lib );
private:
int m_id;
};
#endif // DATABASECOMMAND_SOURCEOFFLINE_H

View File

@@ -0,0 +1,114 @@
#include "databasecommand_updatesearchindex.h"
DatabaseCommand_UpdateSearchIndex::DatabaseCommand_UpdateSearchIndex( const QString& t, int p )
: DatabaseCommand()
, table( t )
, pkey( p )
{
if( table != "artist" && table != "track" && table != "album" )
{
Q_ASSERT(false);
return;
}
}
void DatabaseCommand_UpdateSearchIndex::exec(DatabaseImpl *db)
{
qDebug() << Q_FUNC_INFO;
if( table != "artist" && table != "track" && table != "album" )
{
Q_ASSERT(false);
return;
}
// if pkey is 0, consult DB to see what needs indexing
if( pkey == 0 )
{
TomahawkSqlQuery q = db->newquery();
q.exec( QString("SELECT coalesce(max(id),0) from %1_search_index").arg(table) );
q.next();
pkey = 1 + q.value(0).toInt();
qDebug() << "updateSearchIndex" << table << "consulted DB, starting at" << pkey;
}
TomahawkSqlQuery query = db->newquery();
qDebug() << "Building index for" << table << ">= id" << pkey;
QString searchtable( table + "_search_index" );
query.exec(QString( "SELECT id, sortname FROM %1 WHERE id >= %2" ).arg( table ).arg(pkey ) );
TomahawkSqlQuery upq = db->newquery();
TomahawkSqlQuery inq = db->newquery();
inq.prepare( "INSERT INTO "+ searchtable +" (ngram, id, num) VALUES (?,?,?)" );
upq.prepare( "UPDATE "+ searchtable +" SET num=num+? WHERE ngram=? AND id=?" );
int num_names = 0;
int num_ngrams = 0;
int id;
QString name;
QMap<QString, int> ngrammap;
// this is the new ngram map we build up, to be merged into the
// main one in FuzzyIndex:
QHash< QString, QMap<quint32, quint16> > idx;
while( query.next() )
{
id = query.value( 0 ).toInt();
name = query.value( 1 ).toString();
num_names++;
inq.bindValue( 1, id ); // set id
upq.bindValue( 2, id ); // set id
ngrammap = DatabaseImpl::ngrams( name );
QMapIterator<QString, int> i( ngrammap );
while ( i.hasNext() )
{
i.next();
num_ngrams++;
upq.bindValue( 0, i.value() ); //num
upq.bindValue( 1, i.key() ); // ngram
upq.exec();
if( upq.numRowsAffected() == 0 )
{
inq.bindValue( 0, i.key() ); //ngram
inq.bindValue( 2, i.value() ); //num
inq.exec();
if( inq.numRowsAffected() == 0 )
{
qDebug() << "Error updating search index:" << id << name;
continue;
}
}
// update ngram cache:
QMapIterator<QString, int> iter( ngrammap );
while ( iter.hasNext() )
{
iter.next();
if( idx.contains( iter.key() ) )
{
idx[ iter.key() ][ id ] += iter.value();
}
else
{
QMap<quint32, quint16> tmp;
tmp.insert( id, iter.value() );
idx.insert( iter.key(), tmp );
}
}
}
}
// merge in our ngrams into the main index
QMetaObject::invokeMethod( &(db->m_fuzzyIndex),
"mergeIndex",
Qt::QueuedConnection,
Q_ARG( QString, table ),
QGenericArgument( "QHash< QString, QMap<quint32, quint16> >", &idx )
);
qDebug() << "Finished indexing" << num_names <<" names," << num_ngrams << "ngrams.";
}

View File

@@ -0,0 +1,27 @@
#ifndef DATABASECOMMAND_UPDATESEARCHINDEX_H
#define DATABASECOMMAND_UPDATESEARCHINDEX_H
#include "databasecommand.h"
#include "databaseimpl.h"
class DatabaseCommand_UpdateSearchIndex : public DatabaseCommand
{
Q_OBJECT
public:
explicit DatabaseCommand_UpdateSearchIndex(const QString& table, int pkey);
virtual QString commandname() const { return "updatesearchindex"; }
virtual bool doesMutates() const { return true; }
virtual void exec(DatabaseImpl* db);
signals:
void indexUpdated();
public slots:
private:
QString table;
int pkey;
};
#endif // DATABASECOMMAND_UPDATESEARCHINDEX_H

View File

@@ -0,0 +1,33 @@
#include "databasecommandloggable.h"
#include <QDebug>
#include "database/databasecommand_addfiles.h"
#include "database/databasecommand_setplaylistrevision.h"
DatabaseCommandLoggable*
DatabaseCommandLoggable::factory( QVariantMap c )
{
const QString name = c.value( "command" ).toString();
//TODO dynamic class loading, factory blah
if( name == "addfiles" )
{
DatabaseCommand_AddFiles* cmd = new DatabaseCommand_AddFiles;
QJson::QObjectHelper::qvariant2qobject( c, cmd );
return cmd;
}
else if( name == "setplaylistrevision" )
{
DatabaseCommand_SetPlaylistRevision* cmd = new DatabaseCommand_SetPlaylistRevision;
QJson::QObjectHelper::qvariant2qobject( c, cmd );
return cmd;
}
else
{
qDebug() << "Unhandled command name";
Q_ASSERT( false );
return 0;
}
}

View File

@@ -0,0 +1,29 @@
#ifndef DATABASECOMMANDLOGGABLE_H
#define DATABASECOMMANDLOGGABLE_H
#include "database/databasecommand.h"
/// A Database Command that will be added to the oplog and sent over the network
/// so peers can sync up and changes to our collection in their cached copy.
class DatabaseCommandLoggable : public DatabaseCommand
{
Q_OBJECT
Q_PROPERTY(QString command READ commandname)
public:
explicit DatabaseCommandLoggable( QObject* parent = 0 )
: DatabaseCommand( parent )
{}
explicit DatabaseCommandLoggable( const Tomahawk::source_ptr& s, QObject* parent = 0 )
: DatabaseCommand( s, parent )
{}
virtual bool loggable() const { return true; }
static DatabaseCommandLoggable* factory( QVariantMap c );
};
#endif // DATABASECOMMANDLOGGABLE_H

View File

@@ -0,0 +1,455 @@
#include "databaseimpl.h"
#include <QRegExp>
#include <QStringList>
#include <QtAlgorithms>
#include <QFile>
#include "database.h"
#include "tomahawk/tomahawkapp.h"
#include "databasecommand_updatesearchindex.h"
/* !!!! You need to manually generate schema.sql.h when the schema changes:
cd src/database
./gen_schema.h.sh ./schema.sql tomahawk > schema.sql.h
*/
#include "schema.sql.h"
#define CURRENT_SCHEMA_VERSION 14
DatabaseImpl::DatabaseImpl( const QString& dbname, Database* parent )
: QObject( (QObject*) parent )
, m_lastartid( 0 )
, m_lastalbid( 0 )
, m_lasttrkid( 0 )
, m_fuzzyIndex( *this )
{
connect( this, SIGNAL(indexReady()), parent, SIGNAL(indexReady()) );
db = QSqlDatabase::addDatabase( "QSQLITE", "tomahawk" );
db.setDatabaseName( dbname );
if ( !db.open() )
{
qDebug() << "FAILED TO OPEN DB";
throw "failed to open db"; // TODO
}
QSqlQuery qry = QSqlQuery( db );
query = newquery();
qry.exec( "SELECT v FROM settings WHERE k='schema_version'" );
if ( qry.next() )
{
int v = qry.value( 0 ).toInt();
qDebug() << "Current schema is" << v << this->thread();
if ( v != CURRENT_SCHEMA_VERSION )
{
QString newname = QString("%1.v%2").arg(dbname).arg(v);
qDebug() << endl << "****************************" << endl;
qDebug() << "Schema version too old: " << v << ". Current version is:" << CURRENT_SCHEMA_VERSION;
qDebug() << "Moving" << dbname << newname;
qDebug() << endl << "****************************" << endl;
qry.clear();
query.clear();
qry.finish();
query.finish();
db.close();
db.removeDatabase( "tomahawk" );
if( QFile::rename( dbname, newname ) )
{
db = QSqlDatabase::addDatabase( "QSQLITE", "tomahawk" );
db.setDatabaseName( dbname );
if( !db.open() ) throw "db moving failed";
updateSchema( v );
}
else
{
Q_ASSERT(0);
QTimer::singleShot( 0, APP, SLOT( quit() ) );
return;
}
}
} else {
updateSchema( 0 );
}
query.exec( "SELECT v FROM settings WHERE k='dbid'" );
if( query.next() )
{
m_dbid = query.value( 0 ).toString();
}
else
{
m_dbid = uuid();
query.exec( QString( "INSERT INTO settings(k,v) VALUES('dbid','%1')" ).arg( m_dbid ) );
}
qDebug() << "Database ID:" << m_dbid;
// make sqlite behave how we want:
query.exec( "PRAGMA synchronous = ON" );
query.exec( "PRAGMA foreign_keys = ON" );
//query.exec( "PRAGMA temp_store = MEMORY" );
// in case of unclean shutdown last time:
query.exec( "UPDATE source SET isonline = 'false'" );
}
DatabaseImpl::~DatabaseImpl()
{
m_indexThread.quit();
m_indexThread.wait(5000);
}
void
DatabaseImpl::loadIndex()
{
// load ngram index in the background
m_fuzzyIndex.moveToThread( &m_indexThread );
connect( &m_indexThread, SIGNAL(started()), &m_fuzzyIndex, SLOT(loadNgramIndex()) );
connect( &m_fuzzyIndex, SIGNAL(indexReady()), this, SIGNAL(indexReady()) );
m_indexThread.start();
}
void
DatabaseImpl::updateSearchIndex( const QString& table, int pkey )
{
DatabaseCommand* cmd = new DatabaseCommand_UpdateSearchIndex(table, pkey);
APP->database()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
}
bool
DatabaseImpl::updateSchema( int currentver )
{
qDebug() << "Create tables... old version is" << currentver;
QString sql( get_tomahawk_sql() );
QStringList statements = sql.split( ";", QString::SkipEmptyParts );
db.transaction();
foreach( const QString& sl, statements )
{
QString s( sl.trimmed() );
if( s.length() == 0 )
continue;
qDebug() << "Executing:" << s;
query.exec( s );
}
db.commit();
return true;
}
QVariantMap
DatabaseImpl::file( int fid )
{
QVariantMap m;
query.exec( QString( "SELECT url, mtime, size, md5, mimetype, duration, bitrate, "
"file_join.artist, file_join.album, file_join.track, "
"(select name from artist where id = file_join.artist) as artname, "
"(select name from album where id = file_join.album) as albname, "
"(select name from track where id = file_join.track) as trkname "
"FROM file, file_join "
"WHERE file.id = file_join.file AND file.id = %1" )
.arg( fid ) );
if( query.next() )
{
m["url"] = query.value( 0 ).toString();
m["mtime"] = query.value( 1 ).toString();
m["size"] = query.value( 2 ).toInt();
m["hash"] = query.value( 3 ).toString();
m["mimetype"] = query.value( 4 ).toString();
m["duration"] = query.value( 5 ).toInt();
m["bitrate"] = query.value( 6 ).toInt();
m["artist"] = query.value( 10 ).toString();
m["album"] = query.value( 11 ).toString();
m["track"] = query.value( 12 ).toString();
}
//qDebug() << m;
return m;
}
int
DatabaseImpl::artistId( const QString& name_orig, bool& isnew )
{
isnew = false;
if( m_lastart == name_orig )
return m_lastartid;
int id = 0;
QString sortname = DatabaseImpl::sortname( name_orig );
query.prepare( "SELECT id FROM artist WHERE sortname = ?" );
query.addBindValue( sortname );
query.exec();
if( query.next() )
{
id = query.value( 0 ).toInt();
}
if( id )
{
m_lastart = name_orig;
m_lastartid = id;
return id;
}
// not found, insert it.
query.prepare( "INSERT INTO artist(id,name,sortname) VALUES(NULL,?,?)" );
query.addBindValue( name_orig );
query.addBindValue( sortname );
if( !query.exec() )
{
qDebug() << "Failed to insert artist:" << name_orig;
return 0;
}
id = query.lastInsertId().toInt();
isnew = true;
m_lastart = name_orig;
m_lastartid = id;
return id;
}
int
DatabaseImpl::trackId( int artistid, const QString& name_orig, bool& isnew )
{
isnew = false;
int id = 0;
QString sortname = DatabaseImpl::sortname( name_orig );
//if( ( id = m_artistcache[sortname] ) ) return id;
query.prepare( "SELECT id FROM track WHERE artist = ? AND sortname = ?" );
query.addBindValue( artistid );
query.addBindValue( sortname );
query.exec();
if( query.next() )
{
id = query.value( 0 ).toInt();
}
if( id )
{
//m_trackcache[sortname]=id;
return id;
}
// not found, insert it.
query.prepare( "INSERT INTO track(id,artist,name,sortname) VALUES(NULL,?,?,?)" );
query.addBindValue( artistid );
query.addBindValue( name_orig );
query.addBindValue( sortname );
if( !query.exec() )
{
qDebug() << "Failed to insert track:" << name_orig ;
return 0;
}
id = query.lastInsertId().toInt();
//m_trackcache[sortname]=id;
isnew = true;
return id;
}
int
DatabaseImpl::albumId( int artistid, const QString& name_orig, bool& isnew )
{
isnew = false;
if( name_orig.isEmpty() )
{
//qDebug() << Q_FUNC_INFO << "empty album name";
return 0;
}
if( m_lastartid == artistid && m_lastalb == name_orig )
return m_lastalbid;
int id = 0;
QString sortname = DatabaseImpl::sortname( name_orig );
//if( ( id = m_albumcache[sortname] ) ) return id;
query.prepare( "SELECT id FROM album WHERE artist = ? AND sortname = ?" );
query.addBindValue( artistid );
query.addBindValue( sortname );
query.exec();
if( query.next() )
{
id = query.value( 0 ).toInt();
}
if( id )
{
m_lastalb = name_orig;
m_lastalbid = id;
return id;
}
// not found, insert it.
query.prepare( "INSERT INTO album(id,artist,name,sortname) VALUES(NULL,?,?,?)" );
query.addBindValue( artistid );
query.addBindValue( name_orig );
query.addBindValue( sortname );
if( !query.exec() )
{
qDebug() << "Failed to insert album: " << name_orig ;
return 0;
}
id = query.lastInsertId().toInt();
//m_albumcache[sortname]=id;
isnew = true;
m_lastalb = name_orig;
m_lastalbid = id;
return id;
}
QList< int >
DatabaseImpl::searchTable( const QString& table, const QString& name_orig, uint limit )
{
QList< int > results;
if( table != "artist" && table != "track" && table != "album" )
return results;
QString name = sortname( name_orig );
// first check for exact matches:
query.prepare( QString( "SELECT id FROM %1 WHERE sortname = ?" ).arg( table ) );
query.addBindValue( name );
bool exactok = query.exec();
Q_ASSERT( exactok );
while( query.next() )
results.append( query.value( 0 ).toInt() );
// ngram stuff only works on tracks over a certain length atm:
if( name_orig.length() > 3 )
{
// consult ngram index to find candidates:
QMap< int, float > resultsmap = m_fuzzyIndex.search( table, name );
//qDebug() << "results map for" << table << resultsmap.size();
QList< QPair<int,float> > resultslist;
foreach( int i, resultsmap.keys() )
{
resultslist << QPair<int,float>( i, (float)resultsmap.value( i ) );
}
qSort( resultslist.begin(), resultslist.end(), DatabaseImpl::scorepairSorter );
for( int k = 0; k < resultslist.size() && k < (int)limit; ++k )
{
results << resultslist.at( k ).first;
}
}
return results;
}
QList< int >
DatabaseImpl::getTrackFids( int tid )
{
QList< int > ret;
query.exec( QString( "SELECT file.id FROM file, file_join "
"WHERE file_join.file=file.id "
"AND file_join.track = %1 ").arg( tid ) );
query.exec();
while( query.next() )
ret.append( query.value( 0 ).toInt() );
return ret;
}
QMap< QString, int >
DatabaseImpl::ngrams( const QString& str_orig )
{
static QMap< QString, QMap<QString, int> > memo;
if( memo.contains( str_orig ) )
return memo.value( str_orig );
int n = 3;
QMap<QString, int> ret;
QString str( " " + DatabaseImpl::sortname( str_orig ) + " " );
int num = str.length();
QString ngram;
for( int j = 0; j < num - ( n - 1 ); j++ )
{
ngram = str.mid( j, n );
Q_ASSERT( ngram.length() == n );
if( ret.contains( ngram ) )
ret[ngram]++;
else
ret[ngram] = 1;
}
memo.insert( str_orig, ret );
return ret;
}
QString
DatabaseImpl::sortname( const QString& str )
{
return str.toLower().trimmed().replace( QRegExp("[\\s]{2,}"), " " );
}
QVariantMap
DatabaseImpl::artist( int id )
{
query.exec( QString( "SELECT id, name, sortname FROM artist WHERE id = %1" ).arg( id ) );
QVariantMap m;
if( !query.next() )
return m;
m["id"] = query.value( 0 );
m["name"] = query.value( 1 );
m["sortname"] = query.value( 2 );
return m;
}
QVariantMap
DatabaseImpl::track( int id )
{
query.exec( QString( "SELECT id, artist, name, sortname FROM track WHERE id = %1" ).arg( id ) );
QVariantMap m;
if( !query.next() )
return m;
m["id"] = query.value( 0 );
m["artist"] = query.value( 1 );
m["name"] = query.value( 2 );
m["sortname"] = query.value( 3 );
return m;
}
QVariantMap
DatabaseImpl::album( int id )
{
query.exec( QString( "SELECT id, artist, name, sortname FROM album WHERE id = %1" ).arg( id ) );
QVariantMap m;
if( !query.next() )
return m;
m["id"] = query.value( 0 );
m["artist"] = query.value( 1 );
m["name"] = query.value( 2 );
m["sortname"] = query.value( 3 );
return m;
}

View File

@@ -0,0 +1,79 @@
#ifndef DATABASEIMPL_H
#define DATABASEIMPL_H
#include <QObject>
#include <QList>
#include <QPair>
#include <QVariant>
#include <QVariantMap>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QDebug>
#include <QHash>
#include <QThread>
#include "tomahawksqlquery.h"
#include "fuzzyindex.h"
class Database;
class DatabaseImpl : public QObject
{
Q_OBJECT
friend class FuzzyIndex;
friend class DatabaseCommand_UpdateSearchIndex;
public:
DatabaseImpl( const QString& dbname, Database* parent = 0 );
~DatabaseImpl();
TomahawkSqlQuery newquery() { return TomahawkSqlQuery( db ); }
QSqlDatabase& database() { return db; }
int artistId( const QString& name_orig, bool& isnew );
int trackId( int artistid, const QString& name_orig, bool& isnew );
int albumId( int artistid, const QString& name_orig, bool& isnew );
QList< int > searchTable( const QString& table, const QString& name_orig, uint limit = 10 );
QList< int > getTrackFids( int tid );
static QMap<QString,int> ngrams( const QString& str_orig );
static QString sortname( const QString& str );
QVariantMap artist( int id );
QVariantMap album( int id );
QVariantMap track( int id );
QVariantMap file( int fid );
static bool scorepairSorter( const QPair<int,float>& left, const QPair<int,float>& right )
{
return left.second > right.second;
}
// indexes entries from "table" where id >= pkey
void updateSearchIndex( const QString& table, int pkey );
const QString& dbid() const { return m_dbid; }
void loadIndex();
signals:
void indexReady();
public slots:
private:
bool updateSchema( int currentver );
QSqlDatabase db;
TomahawkSqlQuery query;
QString m_lastart, m_lastalb, m_lasttrk;
int m_lastartid, m_lastalbid, m_lasttrkid;
QString m_dbid;
QThread m_indexThread;
FuzzyIndex m_fuzzyIndex;
};
#endif // DATABASEIMPL_H

View File

@@ -0,0 +1,49 @@
#include "databaseresolver.h"
#include "tomahawk/tomahawkapp.h"
#include "database.h"
#include "database/databasecommand_resolve.h"
DatabaseResolver::DatabaseResolver( bool searchlocal, int weight )
: Resolver()
, m_searchlocal( searchlocal )
, m_weight( weight )
{
}
void
DatabaseResolver::resolve( QVariant v )
{
//qDebug() << Q_FUNC_INFO << v;
if( !m_searchlocal )
{
if( APP->servent().numConnectedPeers() == 0 )
return;
}
DatabaseCommand_Resolve* cmd = new DatabaseCommand_Resolve( v, m_searchlocal );
connect( cmd, SIGNAL( results( Tomahawk::QID, QList< Tomahawk::result_ptr> ) ),
SLOT( gotResults( Tomahawk::QID, QList< Tomahawk::result_ptr> ) ), Qt::QueuedConnection );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
}
void
DatabaseResolver::gotResults( const Tomahawk::QID qid, QList< Tomahawk::result_ptr> results )
{
//qDebug() << Q_FUNC_INFO << qid << results.length();
APP->pipeline()->reportResults( qid, results );
}
QString
DatabaseResolver::name() const
{
return QString( "Database (%1)" ).arg( m_searchlocal ? "local" : "remote" );
}

View File

@@ -0,0 +1,29 @@
#ifndef DATABASERESOLVER_H
#define DATABASERESOLVER_H
#include "tomahawk/resolver.h"
#include "tomahawk/result.h"
class DatabaseResolver : public Tomahawk::Resolver
{
Q_OBJECT
public:
explicit DatabaseResolver( bool searchlocal, int weight );
virtual QString name() const;
virtual unsigned int weight() const { return m_weight; }
virtual unsigned int preference() const { return 100; }
virtual unsigned int timeout() const { return 1000; }
virtual void resolve( QVariant v );
private slots:
void gotResults( const Tomahawk::QID qid, QList< Tomahawk::result_ptr> results );
private:
bool m_searchlocal;
int m_weight;
};
#endif // DATABASERESOLVER_H

View File

@@ -0,0 +1,194 @@
#include "databaseworker.h"
#include <QTimer>
#include <QTime>
#include <QSqlQuery>
#include "tomahawk/tomahawkapp.h"
#include "database.h"
#include "database/databasecommandloggable.h"
DatabaseWorker::DatabaseWorker( DatabaseImpl* lib, Database* db, bool mutates )
: QThread()
, m_dbimpl( lib )
, m_abort( false )
, m_outstanding( 0 )
{
moveToThread( this );
if( mutates )
{
connect( db, SIGNAL( newJobRW(QSharedPointer<DatabaseCommand>) ),
SLOT( doWork(QSharedPointer<DatabaseCommand>) ),
Qt::QueuedConnection );
}
else
{
connect( db, SIGNAL( newJobRO(QSharedPointer<DatabaseCommand>) ),
SLOT( doWork(QSharedPointer<DatabaseCommand>) ),
Qt::QueuedConnection );
}
qDebug() << "CTOR DatabaseWorker" << this->thread();
}
DatabaseWorker::~DatabaseWorker()
{
qDebug() << Q_FUNC_INFO;
quit();
wait( 5000 );
}
void
DatabaseWorker::run()
{
exec();
qDebug() << Q_FUNC_INFO << "DatabaseWorker finishing...";
}
void
DatabaseWorker::doWork( QSharedPointer<DatabaseCommand> cmd )
{
/*
Run the dbcmd. Only inside a transaction if the cmd does mutates.
If the cmd is modifying local content (ie source->isLocal()) then
log to the database oplog for replication to peers.
*/
QTime timer;
timer.start();
if( cmd->doesMutates() )
{
bool transok = m_dbimpl->database().transaction();
Q_ASSERT( transok );
}
try
{
cmd->_exec( m_dbimpl ); // runs actual SQL stuff
if( cmd->loggable() )
{
// We only save our own ops to the oplog, since incoming ops from peers
// are applied immediately.
//
// Crazy idea: if peers had keypairs and could sign ops/msgs, in theory it
// would be safe to sync ops for friend A from friend B's cache, if he saved them,
// which would mean you could get updates even if a peer was offline.
if( cmd->source()->isLocal() )
{
// save to op-log
DatabaseCommandLoggable* command = (DatabaseCommandLoggable*)cmd.data();
logOp( command );
}
else
{
// Make a note of the last guid we applied for this source
// so we can always request just the newer ops in future.
//
qDebug() << "Setting lastop for source" << cmd->source()->id() << "to" << cmd->guid();
TomahawkSqlQuery query = m_dbimpl->newquery();
query.prepare( "UPDATE source SET lastop = ? WHERE id = ?" );
query.addBindValue( cmd->guid() );
query.addBindValue( cmd->source()->id() );
if( !query.exec() )
{
qDebug() << "Failed to set lastop";
throw "Failed to set lastop";
}
}
}
if( cmd->doesMutates() )
{
qDebug() << "Comitting" << cmd->commandname();;
if( !m_dbimpl->database().commit() )
{
qDebug() << "*FAILED TO COMMIT TRANSACTION*";
throw "commit failed";
}
else
{
qDebug() << "Committed" << cmd->commandname();
}
}
//uint duration = timer.elapsed();
//qDebug() << "DBCmd Duration:" << duration << "ms, now running postcommit for" << cmd->commandname();
cmd->postCommit();
//qDebug() << "Post commit finished for"<< cmd->commandname();
}
catch( const char * msg )
{
qDebug() << endl
<< "*ERROR* processing databasecommand:"
<< cmd->commandname()
<< msg
<< m_dbimpl->database().lastError().databaseText()
<< m_dbimpl->database().lastError().driverText()
<< endl;
if( cmd->doesMutates() )
m_dbimpl->database().rollback();
Q_ASSERT( false );
}
catch(...)
{
qDebug() << "Uncaught exception processing dbcmd";
if( cmd->doesMutates() )
m_dbimpl->database().rollback();
Q_ASSERT( false );
throw;
}
cmd->emitFinished();
}
// this should take a const command, need to check/make json stuff mutable for some objs tho maybe.
void
DatabaseWorker::logOp( DatabaseCommandLoggable* command )
{
TomahawkSqlQuery oplogquery = m_dbimpl->newquery();
oplogquery.prepare( "INSERT INTO oplog(source, guid, command, compressed, json) "
"VALUES(?, ?, ?, ?, ?) ");
QVariantMap variant = QJson::QObjectHelper::qobject2qvariant( command );
QByteArray ba = m_serializer.serialize( variant );
//qDebug() << "OP JSON:" << ba; // debug
bool compressed = false;
if( ba.length() >= 512 )
{
// We need to compress this in this thread, since inserting into the log
// has to happen as part of the same transaction as the dbcmd.
// (we are in a worker thread for RW dbcmds anyway, so it's ok)
//qDebug() << "Compressing DB OP JSON, uncompressed size:" << ba.length();
ba = qCompress( ba, 9 );
compressed = true;
//qDebug() << "Compressed DB OP JSON size:" << ba.length();
}
qDebug() << "Saving to oplog:" << command->commandname()
<< "bytes:" << ba.length()
<< "guid:" << command->guid();
oplogquery.bindValue( 0, command->source()->isLocal() ?
QVariant(QVariant::Int) : command->source()->id() );
oplogquery.bindValue( 1, command->guid() );
oplogquery.bindValue( 2, command->commandname() );
oplogquery.bindValue( 3, compressed );
oplogquery.bindValue( 4, ba );
if( !oplogquery.exec() )
{
qDebug() << "Error saving to oplog";
throw "Failed to save to oplog";
}
}

View File

@@ -0,0 +1,46 @@
#ifndef DATABASEWORKER_H
#define DATABASEWORKER_H
#include <QObject>
#include <QThread>
#include <QMutex>
#include <QList>
#include <QSharedPointer>
#include <qjson/parser.h>
#include <qjson/serializer.h>
#include <qjson/qobjecthelper.h>
#include "databasecommand.h"
#include "databaseimpl.h"
class Database;
class DatabaseCommandLoggable;
class DatabaseWorker : public QThread
{
Q_OBJECT
public:
DatabaseWorker( DatabaseImpl*, Database*, bool mutates );
~DatabaseWorker();
//void enqueue( QSharedPointer<DatabaseCommand> );
protected:
void run();
public slots:
void doWork( QSharedPointer<DatabaseCommand> );
private:
void logOp( DatabaseCommandLoggable* command );
QMutex m_mut;
DatabaseImpl* m_dbimpl;
QList< QSharedPointer<DatabaseCommand> > m_commands;
bool m_abort;
int m_outstanding;
QJson::Serializer m_serializer;
};
#endif // DATABASEWORKER_H

124
src/database/fuzzyindex.cpp Normal file
View File

@@ -0,0 +1,124 @@
#include "fuzzyindex.h"
#include "databaseimpl.h"
#include <QTime>
FuzzyIndex::FuzzyIndex(DatabaseImpl &db) :
QObject(), m_db( db ), m_loaded( false )
{
}
void
FuzzyIndex::loadNgramIndex()
{
// this updates the index in the DB, if needed:
qDebug() << "Checking catalogue is fully indexed..";
m_db.updateSearchIndex("artist",0);
m_db.updateSearchIndex("album",0);
m_db.updateSearchIndex("track",0);
// loads index from DB into memory:
qDebug() << "Loading search index for catalogue metadata..." << thread();
loadNgramIndex_helper( m_artist_ngrams, "artist" );
loadNgramIndex_helper( m_album_ngrams, "album" );
loadNgramIndex_helper( m_track_ngrams, "track" );
m_loaded = true;
emit indexReady();
}
void
FuzzyIndex::loadNgramIndex_helper( QHash< QString, QMap<quint32, quint16> >& idx, const QString& table, unsigned int fromkey )
{
QTime t;
t.start();
TomahawkSqlQuery query = m_db.newquery();
query.exec( QString( "SELECT ngram, id, num "
"FROM %1_search_index "
"WHERE id >= %2 "
"ORDER BY ngram" ).arg( table ).arg( fromkey ) );
QMap<quint32, quint16> ngram_idx;
QString lastngram;
while( query.next() )
{
if( lastngram.isEmpty() )
lastngram = query.value(0).toString();
if( query.value( 0 ).toString() != lastngram )
{
idx.insert( lastngram, ngram_idx );
lastngram = query.value( 0 ).toString();
ngram_idx.clear();
}
ngram_idx.insert( query.value( 1 ).toUInt(),
query.value( 2 ).toUInt() );
}
idx.insert( lastngram, ngram_idx );
qDebug() << "Loaded" << idx.size()
<< "ngram entries for" << table
<< "in" << t.elapsed();
}
void FuzzyIndex::mergeIndex(const QString& table, QHash< QString, QMap<quint32, quint16> > tomerge)
{
qDebug() << Q_FUNC_INFO << table << tomerge.keys().size();
QHash< QString, QMap<quint32, quint16> >* idx;
if ( table == "artist" ) idx = &m_artist_ngrams;
else if( table == "album" ) idx = &m_album_ngrams;
else if( table == "track" ) idx = &m_track_ngrams;
else Q_ASSERT(false);
if( tomerge.size() == 0 ) return;
if( idx->size() == 0 )
{
*idx = tomerge;
}
else
{
foreach( const QString& ngram, tomerge.keys() )
{
if( idx->contains( ngram ) )
{
foreach( quint32 id, tomerge[ngram].keys() )
{
(*idx)[ ngram ][ id ] += tomerge[ngram][id];
}
}
else
{
idx->insert( ngram, tomerge[ngram] );
}
}
}
qDebug() << Q_FUNC_INFO << table << "merge complete, num items:" << tomerge.size();
}
QMap< int, float > FuzzyIndex::search( const QString& table, const QString& name )
{
QMap< int, float > resultsmap;
QHash< QString, QMap<quint32, quint16> >* idx;
if( table == "artist" ) idx = &m_artist_ngrams;
else if( table == "album" ) idx = &m_album_ngrams;
else if( table == "track" ) idx = &m_track_ngrams;
QMap<QString,int> ngramsmap = DatabaseImpl::ngrams( name );
foreach( const QString& ngram, ngramsmap.keys() )
{
if( !idx->contains( ngram ) )
continue;
//qDebug() << name_orig << "NGRAM:" << ngram << "candidates:" << (*idx)[ngram].size();
QMapIterator<quint32, quint16> iter( (*idx)[ngram] );
while( iter.hasNext() )
{
iter.next();
resultsmap[ (int) iter.key() ] += (float) iter.value();
}
}
return resultsmap;
}

37
src/database/fuzzyindex.h Normal file
View File

@@ -0,0 +1,37 @@
#ifndef FUZZYINDEX_H
#define FUZZYINDEX_H
#include <QObject>
#include <QMap>
#include <QHash>
#include <QString>
class DatabaseImpl;
class FuzzyIndex : public QObject
{
Q_OBJECT
public:
explicit FuzzyIndex( DatabaseImpl &db );
signals:
void indexReady();
public slots:
void loadNgramIndex();
QMap< int, float > search( const QString& table, const QString& name );
void mergeIndex(const QString& table, QHash< QString, QMap<quint32, quint16> > tomerge);
private:
void loadNgramIndex_helper( QHash< QString, QMap<quint32, quint16> >& idx, const QString& table, unsigned int fromkey = 0);
// maps an ngram to {track id, num occurences}
QHash< QString, QMap<quint32, quint16> > m_artist_ngrams, m_album_ngrams, m_track_ngrams;
DatabaseImpl & m_db;
bool m_loaded;
};
#endif // FUZZYINDEX_H

28
src/database/gen_schema.h.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/bash
schema=$1
name=$2
if [ -e "$schema" -a -n "$name" ]
then
cat <<EOF
/*
This file was automatically generated from $schema on `date`.
*/
static const char * ${name}_schema_sql =
EOF
awk '!/^-/ && length($0) {gsub(/[ \t]+$/, "", $0); gsub("\"","\\\"",$0); gsub("--.*$","",$0); printf("\"%s\"\n",$0);}' "$schema"
cat <<EOF
;
const char * get_${name}_sql()
{
return ${name}_schema_sql;
}
EOF
else
echo "Usage: $0 <schema.sql> <name>"
exit 1
fi

17
src/database/op.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef OP_H
#define OP_H
#include <QString>
#include <QByteArray>
#include <QSharedPointer>
struct DBOp
{
QString guid;
QString command;
QByteArray payload;
bool compressed;
};
typedef QSharedPointer<DBOp> dbop_ptr;
#endif // OP_H

218
src/database/schema.sql Normal file
View File

@@ -0,0 +1,218 @@
-- Mutates to the database are entered into the transaction log
-- so they can be sent to peers to replay against a cache of your DB.
-- This allows peers to get diffs/sync your collection easily.
CREATE TABLE IF NOT EXISTS oplog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE, -- DEFERRABLE INITIALLY DEFERRED,
guid TEXT NOT NULL,
command TEXT NOT NULL,
compressed BOOLEAN NOT NULL,
json TEXT NOT NULL
);
CREATE UNIQUE INDEX oplog_guid ON oplog(guid);
CREATE INDEX oplog_source ON oplog(source);
-- the basic 3 catalogue tables:
CREATE TABLE IF NOT EXISTS artist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
sortname TEXT NOT NULL
);
CREATE UNIQUE INDEX artist_sortname ON artist(sortname);
CREATE TABLE IF NOT EXISTS track (
id INTEGER PRIMARY KEY AUTOINCREMENT,
artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
name TEXT NOT NULL,
sortname TEXT NOT NULL
);
CREATE UNIQUE INDEX track_artist_sortname ON track(artist,sortname);
CREATE TABLE IF NOT EXISTS album (
id INTEGER PRIMARY KEY AUTOINCREMENT,
artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
name TEXT NOT NULL,
sortname TEXT NOT NULL
);
CREATE UNIQUE INDEX album_artist_sortname ON album(artist,sortname);
-- Source, typically a remote peer.
CREATE TABLE IF NOT EXISTS source (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
friendlyname TEXT,
lastop TEXT NOT NULL DEFAULT "", -- guid of last op we've successfully applied
isonline BOOLEAN NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX source_name ON source(name);
-- playlists
CREATE TABLE IF NOT EXISTS playlist (
guid TEXT PRIMARY KEY,
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, -- owner
shared BOOLEAN DEFAULT false,
title TEXT,
info TEXT,
creator TEXT,
lastmodified INTEGER NOT NULL DEFAULT 0,
currentrevision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED
);
--INSERT INTO playlist(guid, title, info, currentrevision)
-- VALUES('playlistguid-1','Test Playlist','this playlist automatically created and used for testing','revisionguid-1');
CREATE TABLE IF NOT EXISTS playlist_item (
guid TEXT PRIMARY KEY,
playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
trackname TEXT NOT NULL,
artistname TEXT NOT NULL,
albumname TEXT,
annotation TEXT,
duration INTEGER, -- in seconds, even tho xspf uses milliseconds
addedon INTEGER NOT NULL DEFAULT 0, -- date added to playlist
addedby INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, -- who added this to the playlist
result_hint TEXT -- hint as to a result, to avoid using the resolver
);
CREATE INDEX playlist_item_playlist ON playlist_item(playlist);
--INSERT INTO playlist_item(guid, playlist, trackname, artistname)
-- VALUES('itemguid-1','playlistguid-1','track name 01','artist name 01');
--INSERT INTO playlist_item(guid, playlist, trackname, artistname)
-- VALUES('itemguid-2','playlistguid-1','track name 02','artist name 02');
--INSERT INTO playlist_item(guid, playlist, trackname, artistname)
-- VALUES('itemguid-3','playlistguid-1','track name 03','artist name 03');
--
CREATE TABLE IF NOT EXISTS playlist_revision (
guid TEXT PRIMARY KEY,
playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
entries TEXT, -- qlist( guid, guid... )
author INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
timestamp INTEGER NOT NULL DEFAULT 0,
previous_revision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED
);
--INSERT INTO playlist_revision(guid, playlist, entries)
-- VALUES('revisionguid-1', 'playlistguid-1', '["itemguid-2","itemguid-1","itemguid-3"]');
-- the trigram search indexes
CREATE TABLE IF NOT EXISTS artist_search_index (
ngram TEXT NOT NULL,
id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
num INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(ngram, id)
);
-- CREATE INDEX artist_search_index_ngram ON artist_search_index(ngram);
CREATE TABLE IF NOT EXISTS album_search_index (
ngram TEXT NOT NULL,
id INTEGER NOT NULL REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
num INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(ngram, id)
);
-- CREATE INDEX album_search_index_ngram ON album_search_index(ngram);
CREATE TABLE IF NOT EXISTS track_search_index (
ngram TEXT NOT NULL,
id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
num INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(ngram, id)
);
-- CREATE INDEX track_search_index_ngram ON track_search_index(ngram);
-- files on disk and joinage with catalogue. physical properties of files only:
-- if source=null, file is local to this machine
CREATE TABLE IF NOT EXISTS file (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
url TEXT NOT NULL, -- file:///music/foo/bar.mp3, <guid or hash?>
size INTEGER NOT NULL, -- in bytes
mtime INTEGER NOT NULL, -- file mtime, so we know to rescan
md5 TEXT, -- useful when comparing stuff p2p
mimetype TEXT, -- "audio/mpeg"
duration INTEGER NOT NULL DEFAULT 0, -- seconds
bitrate INTEGER NOT NULL DEFAULT 0 -- kbps (or equiv)
);
CREATE UNIQUE INDEX file_url_src_uniq ON file(source, url);
CREATE INDEX file_source ON file(source);
-- mtime of dir when last scanned.
-- load into memory when rescanning, skip stuff that's unchanged
CREATE TABLE IF NOT EXISTS dirs_scanned (
name TEXT PRIMARY KEY,
mtime INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS file_join (
file INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
track INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
album INTEGER REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
albumpos INTEGER
);
CREATE INDEX file_join_track ON file_join(track);
CREATE INDEX file_join_artist ON file_join(artist);
CREATE INDEX file_join_album ON file_join(album);
-- tags, weighted and by source (rock, jazz etc)
-- weight is always 1.0 if tag provided by our user.
-- may be less from aggregate sources like lastfm global tags
CREATE TABLE IF NOT EXISTS track_tags (
id INTEGER PRIMARY KEY, -- track id
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
tag TEXT NOT NULL, -- always store as lowercase
ns TEXT, -- ie 'last.fm', 'echonest'
weight float DEFAULT 1.0 -- range 0-1
);
CREATE INDEX track_tags_tag ON track_tags(tag);
CREATE TABLE IF NOT EXISTS album_tags (
id INTEGER PRIMARY KEY, -- album id
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
tag TEXT NOT NULL, -- always store as lowercase
ns TEXT, -- ie 'last.fm', 'echonest'
weight float DEFAULT 1.0 -- range 0-1
);
CREATE INDEX album_tags_tag ON album_tags(tag);
CREATE TABLE IF NOT EXISTS artist_tags (
id INTEGER PRIMARY KEY, -- artist id
source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
tag TEXT NOT NULL, -- always store as lowercase
ns TEXT, -- ie 'last.fm', 'echonest'
weight float DEFAULT 1.0 -- range 0-1
);
CREATE INDEX artist_tags_tag ON artist_tags(tag);
-- all other attributes.
-- like tags that have a value, eg:
-- BPM=120, releaseyear=1980, key=Dminor, composer=Someone
-- NB: since all values are text, numeric values should be zero-padded to a set amount
-- so that we can always do range queries.
CREATE TABLE IF NOT EXISTS track_attributes (
id INTEGER NOT NULL, -- track id
k TEXT NOT NULL,
v TEXT NOT NULL
);
CREATE INDEX track_attrib_id ON track_attributes(id);
CREATE INDEX track_attrib_k ON track_attributes(k);
-- Schema version, and misc tomahawk settings relating to the collection db
CREATE TABLE IF NOT EXISTS settings (
k TEXT NOT NULL PRIMARY KEY,
v TEXT NOT NULL DEFAULT ''
);
INSERT INTO settings(k,v) VALUES('schema_version', '14');

163
src/database/schema.sql.h Normal file
View File

@@ -0,0 +1,163 @@
/*
This file was automatically generated from schema.sql on Tue Jul 13 12:23:44 CEST 2010.
*/
static const char * tomahawk_schema_sql =
"CREATE TABLE IF NOT EXISTS oplog ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE, "
" guid TEXT NOT NULL,"
" command TEXT NOT NULL,"
" compressed BOOLEAN NOT NULL,"
" json TEXT NOT NULL"
");"
"CREATE UNIQUE INDEX oplog_guid ON oplog(guid);"
"CREATE INDEX oplog_source ON oplog(source);"
"CREATE TABLE IF NOT EXISTS artist ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" sortname TEXT NOT NULL"
");"
"CREATE UNIQUE INDEX artist_sortname ON artist(sortname);"
"CREATE TABLE IF NOT EXISTS track ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" name TEXT NOT NULL,"
" sortname TEXT NOT NULL"
");"
"CREATE UNIQUE INDEX track_artist_sortname ON track(artist,sortname);"
"CREATE TABLE IF NOT EXISTS album ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" name TEXT NOT NULL,"
" sortname TEXT NOT NULL"
");"
"CREATE UNIQUE INDEX album_artist_sortname ON album(artist,sortname);"
"CREATE TABLE IF NOT EXISTS source ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" friendlyname TEXT,"
" lastop TEXT NOT NULL DEFAULT \"\", "
" isonline BOOLEAN NOT NULL DEFAULT false"
");"
"CREATE UNIQUE INDEX source_name ON source(name);"
"CREATE TABLE IF NOT EXISTS playlist ("
" guid TEXT PRIMARY KEY,"
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, "
" shared BOOLEAN DEFAULT false,"
" title TEXT,"
" info TEXT,"
" creator TEXT,"
" lastmodified INTEGER NOT NULL DEFAULT 0,"
" currentrevision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED"
");"
"CREATE TABLE IF NOT EXISTS playlist_item ("
" guid TEXT PRIMARY KEY,"
" playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" trackname TEXT NOT NULL,"
" artistname TEXT NOT NULL,"
" albumname TEXT,"
" annotation TEXT,"
" duration INTEGER, "
" addedon INTEGER NOT NULL DEFAULT 0, "
" addedby INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED, "
" result_hint TEXT "
");"
"CREATE INDEX playlist_item_playlist ON playlist_item(playlist);"
"CREATE TABLE IF NOT EXISTS playlist_revision ("
" guid TEXT PRIMARY KEY,"
" playlist TEXT NOT NULL REFERENCES playlist(guid) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" entries TEXT, "
" author INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" timestamp INTEGER NOT NULL DEFAULT 0,"
" previous_revision TEXT REFERENCES playlist_revision(guid) DEFERRABLE INITIALLY DEFERRED"
");"
"CREATE TABLE IF NOT EXISTS artist_search_index ("
" ngram TEXT NOT NULL,"
" id INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" num INTEGER NOT NULL DEFAULT 1,"
" PRIMARY KEY(ngram, id)"
");"
"CREATE TABLE IF NOT EXISTS album_search_index ("
" ngram TEXT NOT NULL,"
" id INTEGER NOT NULL REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" num INTEGER NOT NULL DEFAULT 1,"
" PRIMARY KEY(ngram, id)"
");"
"CREATE TABLE IF NOT EXISTS track_search_index ("
" ngram TEXT NOT NULL,"
" id INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" num INTEGER NOT NULL DEFAULT 1,"
" PRIMARY KEY(ngram, id)"
");"
"CREATE TABLE IF NOT EXISTS file ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" url TEXT NOT NULL, "
" size INTEGER NOT NULL, "
" mtime INTEGER NOT NULL, "
" md5 TEXT, "
" mimetype TEXT, "
" duration INTEGER NOT NULL DEFAULT 0, "
" bitrate INTEGER NOT NULL DEFAULT 0 "
");"
"CREATE UNIQUE INDEX file_url_src_uniq ON file(source, url);"
"CREATE INDEX file_source ON file(source);"
"CREATE TABLE IF NOT EXISTS dirs_scanned ("
" name TEXT PRIMARY KEY,"
" mtime INTEGER NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS file_join ("
" file INTEGER PRIMARY KEY REFERENCES file(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" artist INTEGER NOT NULL REFERENCES artist(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" track INTEGER NOT NULL REFERENCES track(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" album INTEGER REFERENCES album(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" albumpos INTEGER"
");"
"CREATE INDEX file_join_track ON file_join(track);"
"CREATE INDEX file_join_artist ON file_join(artist);"
"CREATE INDEX file_join_album ON file_join(album);"
"CREATE TABLE IF NOT EXISTS track_tags ("
" id INTEGER PRIMARY KEY, "
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" tag TEXT NOT NULL, "
" ns TEXT, "
" weight float DEFAULT 1.0 "
");"
"CREATE INDEX track_tags_tag ON track_tags(tag);"
"CREATE TABLE IF NOT EXISTS album_tags ("
" id INTEGER PRIMARY KEY, "
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" tag TEXT NOT NULL, "
" ns TEXT, "
" weight float DEFAULT 1.0 "
");"
"CREATE INDEX album_tags_tag ON album_tags(tag);"
"CREATE TABLE IF NOT EXISTS artist_tags ("
" id INTEGER PRIMARY KEY, "
" source INTEGER REFERENCES source(id) ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,"
" tag TEXT NOT NULL, "
" ns TEXT, "
" weight float DEFAULT 1.0 "
");"
"CREATE INDEX artist_tags_tag ON artist_tags(tag);"
"CREATE TABLE IF NOT EXISTS track_attributes ("
" id INTEGER NOT NULL, "
" k TEXT NOT NULL,"
" v TEXT NOT NULL"
");"
"CREATE INDEX track_attrib_id ON track_attributes(id);"
"CREATE INDEX track_attrib_k ON track_attributes(k);"
"CREATE TABLE IF NOT EXISTS settings ("
" k TEXT NOT NULL PRIMARY KEY,"
" v TEXT NOT NULL DEFAULT ''"
");"
"INSERT INTO settings(k,v) VALUES('schema_version', '14');"
;
const char * get_tomahawk_sql()
{
return tomahawk_schema_sql;
}

View File

@@ -0,0 +1,59 @@
#ifndef TOMAHAWKSQLQUERY_H
#define TOMAHAWKSQLQUERY_H
// subclass QSqlQuery so that it prints the error msg if a query fails
#include <QSqlQuery>
#include <QTime>
#define TOMAHAWK_QUERY_THRESHOLD 20
class TomahawkSqlQuery : public QSqlQuery
{
public:
TomahawkSqlQuery()
: QSqlQuery()
{}
TomahawkSqlQuery( QSqlDatabase db )
: QSqlQuery( db )
{}
bool exec( const QString& query )
{
prepare( query );
return exec();
}
bool exec()
{
QTime t;
t.start();
bool ret = QSqlQuery::exec();
if( !ret )
showError();
int e = t.elapsed();
if ( e >= TOMAHAWK_QUERY_THRESHOLD )
qDebug() << "TomahawkSqlQuery (" << lastQuery() << ") finished in" << t.elapsed() << "ms";
return ret;
}
private:
void showError()
{
qDebug()
<< endl << "*** DATABASE ERROR ***" << endl
<< this->lastQuery() << endl
<< "boundValues:" << this->boundValues() << endl
<< this->lastError().text() << endl
;
Q_ASSERT( false );
}
};
#endif // TOMAHAWKSQLQUERY_H

301
src/dbsyncconnection.cpp Normal file
View File

@@ -0,0 +1,301 @@
/*
Database syncing using the oplog table.
=======================================
Load the last GUID we applied for the peer, tell them it.
In return, they send us all new ops since that guid.
We then apply those new ops to our cache of their data
Synced.
*/
#include "dbsyncconnection.h"
#include <QDebug>
#include "tomahawk/tomahawkapp.h"
#include "tomahawk/source.h"
#include "database.h"
#include "databasecommand.h"
#include "databasecommand_collectionstats.h"
#include "databasecommand_loadops.h"
#include "remotecollection.h"
// close the dbsync connection after this much inactivity.
// it's automatically reestablished as needed.
#define IDLE_TIMEOUT 60000
using namespace Tomahawk;
DBSyncConnection::DBSyncConnection( Servent* s, source_ptr src )
: Connection( s )
, m_source( src )
, m_state( UNKNOWN )
{
qDebug() << Q_FUNC_INFO << thread();
connect( this, SIGNAL( stateChanged( DBSyncConnection::State, DBSyncConnection::State, QString ) ),
m_source.data(), SIGNAL( loadingStateChanged(DBSyncConnection::State, DBSyncConnection::State, QString ) )
);
m_timer.setInterval( IDLE_TIMEOUT );
connect( &m_timer, SIGNAL( timeout() ), SLOT( idleTimeout() ) );
this->setMsgProcessorModeIn( MsgProcessor::PARSE_JSON | MsgProcessor::UNCOMPRESS_ALL );
// msgs are stored compressed in the db, so not typically needed here, but doesnt hurt:
this->setMsgProcessorModeOut( MsgProcessor::COMPRESS_IF_LARGE );
}
DBSyncConnection::~DBSyncConnection()
{
qDebug() << "DTOR" << Q_FUNC_INFO;
}
void
DBSyncConnection::idleTimeout()
{
qDebug() << Q_FUNC_INFO << "*************";
shutdown(true);
}
void
DBSyncConnection::changeState( State newstate )
{
State s = m_state;
m_state = newstate;
qDebug() << "DBSYNC State changed from" << s << "to" << newstate;
emit stateChanged( newstate, s, "" );
if( newstate == SYNCED )
{
qDebug() << "Synced :)";
}
}
void
DBSyncConnection::setup()
{
qDebug() << Q_FUNC_INFO;
setId( QString("DBSyncConnection/%1").arg(socket()->peerAddress().toString()) );
check();
}
void
DBSyncConnection::trigger()
{
qDebug() << Q_FUNC_INFO;
// if we're still setting up the connection, do nothing - we sync on first connect anyway:
if( !this->isRunning() ) return;
QMetaObject::invokeMethod( this, "sendMsg", Qt::QueuedConnection,
Q_ARG( msg_ptr,
Msg::factory( "{\"method\":\"trigger\"}", Msg::JSON ) )
);
}
void
DBSyncConnection::check()
{
qDebug() << Q_FUNC_INFO;
if( m_state != UNKNOWN && m_state != SYNCED )
{
qDebug() << "Syncing in progress already.";
return;
}
m_uscache.clear();
m_themcache.clear();
m_us.clear();
changeState(CHECKING);
// load last-modified etc data for our collection and theirs from our DB:
DatabaseCommand_CollectionStats * cmd_us =
new DatabaseCommand_CollectionStats( APP->sourcelist().getLocal() );
DatabaseCommand_CollectionStats * cmd_them =
new DatabaseCommand_CollectionStats( m_source );
connect( cmd_us, SIGNAL( done(const QVariantMap&) ),
this, SLOT( gotUs(const QVariantMap&) ) );
connect( cmd_them, SIGNAL( done(const QVariantMap&) ),
this, SLOT( gotThemCache(const QVariantMap&) ) );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd_us) );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd_them) );
// restarts idle countdown
m_timer.start();
}
/// Called once we've loaded our mtimes etc from the DB for our local
/// collection - send them to the remote peer to compare.
void
DBSyncConnection::gotUs( const QVariantMap& m )
{
m_us = m;
if( !m_uscache.empty() ) sendOps();
}
/// Called once we've loaded our cached data about their collection
void
DBSyncConnection::gotThemCache( const QVariantMap& m )
{
m_themcache = m;
qDebug() << "Sending a FETCHOPS cmd since:" << m.value("lastop").toString();
changeState(FETCHING);
QVariantMap msg;
msg.insert( "method", "fetchops" );
msg.insert( "lastop", m_themcache.value("lastop").toString() );
sendMsg( msg );
}
void
DBSyncConnection::handleMsg( msg_ptr msg )
{
Q_ASSERT( !msg->is( Msg::COMPRESSED ) );
if( m_state == FETCHING ) changeState(PARSING);
// "everything is synced" indicated by non-json msg containing "ok":
if( !msg->is( Msg::JSON ) &&
msg->is( Msg::DBOP ) &&
msg->payload() == "ok" )
{
qDebug() << "No ops to apply, we are synced.";
changeState(SYNCED);
// calc the collection stats, to updates the "X tracks" in the sidebar etc
// this is done automatically if you run a dbcmd to add files.
DatabaseCommand_CollectionStats * cmd = new DatabaseCommand_CollectionStats( m_source );
connect( cmd, SIGNAL( done( const QVariantMap & ) ),
m_source.data(), SLOT( setStats( const QVariantMap& ) ), Qt::QueuedConnection );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );
return;
}
Q_ASSERT( msg->is( Msg::JSON ) );
QVariantMap m = msg->json().toMap();
//qDebug() << ">>>>" << m;
if( m.empty() )
{
qDebug() << "Failed to parse msg in dbsync";
Q_ASSERT( false );
return;
}
// a db sync op msg
if( msg->is( Msg::DBOP ) )
{
DatabaseCommand * cmd = DatabaseCommand::factory( m, m_source );
Q_ASSERT( cmd );
qDebug() << "APPLYING CMD" << cmd->commandname() << cmd->guid();
if( !msg->is( Msg::FRAGMENT ) ) // last msg in this batch
{
changeState( SAVING ); // just DB work left to complete
connect( cmd, SIGNAL( finished() ), this, SLOT( lastOpApplied() ) );
}
APP->database()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
return;
}
if( m.value( "method" ).toString() == "fetchops" )
{
m_uscache = m;
if( !m_us.empty() ) sendOps();
return;
}
if( m.value( "method" ).toString() == "trigger" )
{
qDebug() << "Got trigger msg on dbsyncconnection, checking for new stuff.";
check();
return;
}
qDebug() << Q_FUNC_INFO << "Unhandled msg: " << msg->payload();
Q_ASSERT( false );
}
void
DBSyncConnection::lastOpApplied()
{
qDebug() << Q_FUNC_INFO;
changeState(SYNCED);
// check again, until peer reponds we have no new ops to process
check();
}
/// request new copies of anything we've cached that is stale
void
DBSyncConnection::sendOps()
{
qDebug() << Q_FUNC_INFO;
const QString sinceguid = m_uscache.value( "lastop" ).toString();
qDebug() << "Will send peer all ops since" << sinceguid;
source_ptr src = APP->sourcelist().getLocal();
DatabaseCommand_loadOps * cmd = new DatabaseCommand_loadOps( src, sinceguid );
connect( cmd, SIGNAL( done( QString, QList< dbop_ptr > ) ),
this, SLOT( sendOpsData( QString, QList< dbop_ptr > ) ) );
APP->database()->enqueue( QSharedPointer<DatabaseCommand>( cmd ) );
}
void
DBSyncConnection::sendOpsData( QString sinceguid, QList< dbop_ptr > ops )
{
qDebug() << Q_FUNC_INFO << sinceguid << "Num ops to send: " << ops.length();
if( ops.length() == 0 )
{
sendMsg( Msg::factory( "ok", Msg::DBOP ) );
return;
}
int i;
for( i = 0; i < ops.length(); ++i )
{
quint8 flags = Msg::JSON | Msg::DBOP;
if( ops.at(i)->compressed )
flags |= Msg::COMPRESSED;
if( i != ops.length()-1 )
flags |= Msg::FRAGMENT;
sendMsg( Msg::factory( ops.at(i)->payload, flags ) );
}
}
Connection*
DBSyncConnection::clone()
{
Q_ASSERT( false );
return 0;
}

67
src/dbsyncconnection.h Normal file
View File

@@ -0,0 +1,67 @@
#ifndef DBSYNCCONNECTION_H
#define DBSYNCCONNECTION_H
#include <QObject>
#include <QTimer>
#include <QSharedPointer>
#include <QIODevice>
#include "connection.h"
#include "database/op.h"
#include "tomahawk/typedefs.h"
class DBSyncConnection : public Connection
{
Q_OBJECT
public:
enum State
{
UNKNOWN,
CHECKING,
FETCHING,
PARSING,
SAVING,
SYNCED,
SCANNING
};
explicit DBSyncConnection( Servent* s, Tomahawk::source_ptr src );
virtual ~DBSyncConnection();
void setup();
Connection* clone();
signals:
void stateChanged( DBSyncConnection::State newstate, DBSyncConnection::State oldstate, const QString& info );
protected slots:
virtual void handleMsg( msg_ptr msg );
public slots:
void sendOps();
/// trigger a re-sync to pick up any new ops
void trigger();
private slots:
void gotUs( const QVariantMap& m );
void gotThemCache( const QVariantMap& m );
void lastOpApplied();
void sendOpsData( QString sinceguid, QList< dbop_ptr > ops );
void check();
void idleTimeout();
private:
void compareAndRequest();
void synced();
void changeState( State newstate );
Tomahawk::source_ptr m_source;
QVariantMap m_us, m_uscache, m_themcache;
State m_state;
QTimer m_timer;
};
#endif // DBSYNCCONNECTION_H

View File

@@ -0,0 +1,187 @@
#include "filetransferconnection.h"
#include <QFile>
#include "tomahawk/tomahawkapp.h"
#include "tomahawk/result.h"
#include "bufferiodevice.h"
#include "controlconnection.h"
#include "databasecommand_loadfile.h"
#include "database.h"
// Msgs are framed, this is the size each msg we send containing audio data:
#define BLOCKSIZE 4096
using namespace Tomahawk;
FileTransferConnection::FileTransferConnection( Servent* s, ControlConnection* cc,
QString fid, unsigned int size )
: Connection( s )
, m_cc( cc )
, m_fid( fid )
, m_type( RECEIVING )
, m_badded( 0 )
, m_bsent( 0 )
, m_allok( false )
{
qDebug() << Q_FUNC_INFO;
BufferIODevice * bio = new BufferIODevice(size);
m_iodev = QSharedPointer<QIODevice>( bio ); // device audio data gets written to
m_iodev->open( QIODevice::ReadWrite );
APP->servent().registerFileTransferConnection( this );
// if the audioengine closes the iodev (skip/stop/etc) then kill the connection
// immediately to avoid unnecessary network transfer
connect( m_iodev.data(), SIGNAL( aboutToClose() ), this, SLOT( shutdown() ), Qt::QueuedConnection );
// auto delete when connection closes:
connect( this, SIGNAL( finished() ), SLOT( deleteLater() ), Qt::QueuedConnection );
// don't fuck with our messages at all. No compression, no parsing, nothing:
this->setMsgProcessorModeIn( MsgProcessor::NOTHING );
this->setMsgProcessorModeOut( MsgProcessor::NOTHING );
}
FileTransferConnection::FileTransferConnection( Servent* s, QString fid )
: Connection(s), m_fid(fid), m_type(SENDING), m_badded(0), m_bsent(0), m_allok( false )
{
APP->servent().registerFileTransferConnection( this );
// auto delete when connection closes:
connect( this, SIGNAL( finished() ), SLOT( deleteLater() ), Qt::QueuedConnection );
}
FileTransferConnection::~FileTransferConnection()
{
qDebug() << Q_FUNC_INFO << "TX/RX:" << bytesSent() << bytesReceived();
if( m_type == RECEIVING && !m_allok )
{
qDebug() << "FTConnection closing before last data msg received, shame.";
//TODO log the fact that our peer was bad-mannered enough to not finish the upload
// protected, we could expose it:
//m_iodev->setErrorString("FTConnection providing data went away mid-transfer");
}
APP->servent().fileTransferFinished( this );
}
QString
FileTransferConnection::id() const
{
return QString("FTC[%1 %2]")
.arg( m_type == SENDING ? "TX" : "RX" )
.arg(m_fid);
}
void
FileTransferConnection::showStats( qint64 tx, qint64 rx )
{
if( tx > 0 || rx > 0 )
{
qDebug() << id()
<< QString("Down: %L1 bytes/sec, ").arg(rx)
<< QString("Up: %L1 bytes/sec").arg(tx);
}
}
void
FileTransferConnection::setup()
{
connect( this, SIGNAL( statsTick( qint64, qint64 ) ), SLOT( showStats( qint64, qint64 ) ) );
if(m_type == RECEIVING)
{
qDebug() << "in RX mode";
return;
}
qDebug() << "in TX mode, fid:" << m_fid;
DatabaseCommand_LoadFile * cmd = new DatabaseCommand_LoadFile(m_fid);
connect(cmd, SIGNAL(result(QVariantMap)), this, SLOT(startSending(QVariantMap)));
TomahawkApp::instance()->database()->enqueue(QSharedPointer<DatabaseCommand>(cmd));
}
void
FileTransferConnection::startSending( QVariantMap f )
{
Tomahawk::result_ptr result(new Tomahawk::Result(f, collection_ptr()));
qDebug() << "Starting to transmit" << result->url();
QSharedPointer<QIODevice> io = TomahawkApp::instance()->getIODeviceForUrl(result);
if(!io)
{
qDebug() << "Couldn't read from source:" << result->url();
shutdown();
return;
}
m_readdev = QSharedPointer<QIODevice>(io);
sendSome();
}
void
FileTransferConnection::handleMsg( msg_ptr msg )
{
Q_ASSERT(m_type == FileTransferConnection::RECEIVING);
Q_ASSERT( msg->is( Msg::RAW ) );
m_badded += msg->payload().length();
((BufferIODevice*)m_iodev.data())->addData( msg->payload() );
//qDebug() << Q_FUNC_INFO << "flags" << (int) msg->flags()
// << "payload len" << msg->payload().length()
// << "written to device so far: " << m_badded;
if( !msg->is( Msg::FRAGMENT ) )
{
qDebug() << endl
<< "*** Got last msg in filetransfer. added" << m_badded
<< "io size" << m_iodev->size()
<< endl;
m_allok = true;
// tell our iodev there is no more data to read, no args meaning a success:
((BufferIODevice*)m_iodev.data())->inputComplete();
shutdown();
}
}
Connection* FileTransferConnection::clone()
{
Q_ASSERT(false); return 0;
}
void FileTransferConnection::sendSome()
{
Q_ASSERT(m_type == FileTransferConnection::SENDING);
QByteArray ba = m_readdev->read(BLOCKSIZE);
m_bsent += ba.length();
//qDebug() << "Sending " << ba.length() << " bytes of audiofile";
if( m_readdev->atEnd() )
{
sendMsg( Msg::factory( ba, Msg::RAW ) );
qDebug() << "Sent all. DONE. " << m_bsent;
shutdown(true);
return;
}
else
{
// more to come -> FRAGMENT
sendMsg( Msg::factory( ba, Msg::RAW | Msg::FRAGMENT ) );
}
// HINT: change the 0 to 50 to transmit at 640Kbps, for example
// (this is where upload throttling could be implemented)
QTimer::singleShot( 0, this, SLOT( sendSome() ) );
}

View File

@@ -0,0 +1,62 @@
#ifndef FILETRANSFERCONNECTION_H
#define FILETRANSFERCONNECTION_H
#include <QObject>
#include <QSharedPointer>
#include <QIODevice>
#include "connection.h"
class ControlConnection;
class BufferIODevice;
class FileTransferConnection : public Connection
{
Q_OBJECT
public:
enum Type
{
SENDING = 0,
RECEIVING = 1
};
// RX:
explicit FileTransferConnection( Servent* s, ControlConnection* parent, QString fid, unsigned int size );
// TX:
explicit FileTransferConnection( Servent* s, QString fid );
virtual ~FileTransferConnection();
QString id() const;
void setup();
Connection* clone();
const QSharedPointer<QIODevice>& iodevice()
{
return m_iodev;
}
Type type() const { return m_type; }
QString fid() const { return m_fid; }
protected slots:
virtual void handleMsg( msg_ptr msg );
private slots:
void startSending( QVariantMap );
void sendSome();
void showStats(qint64 tx, qint64 rx);
private:
QSharedPointer<QIODevice> m_iodev;
ControlConnection* m_cc;
QString m_fid;
Type m_type;
QSharedPointer<QIODevice> m_readdev;
int m_badded, m_bsent;
bool m_allok; // got last msg ok, transfer complete?
};
#endif // FILETRANSFERCONNECTION_H

18
src/headlesscheck.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef HEADLESSCHECK
#define HEADLESSCHECK
#ifdef ENABLE_HEADLESS
#define TOMAHAWK_APPLICATION QCoreApplication
#define TOMAHAWK_HEADLESS
#include <QCoreApplication>
#else
#define TOMAHAWK_APPLICATION QApplication
#include <QApplication>
#include "tomahawkwindow.h"
#endif
#endif

100
src/jabber/jabber.h Normal file
View File

@@ -0,0 +1,100 @@
#ifndef JABBER_H
#define JABBER_H
/*
Pimpl of jabber_p, which inherits from a gazillion gloox classes
and it littered with public methods.
*/
#include "jabber_p.h"
class Jabber : public QObject
{
Q_OBJECT
public:
Jabber(const QString &jid,
const QString password,
const QString server = "",
const int port=-1)
: p( jid, password, server, port )
{
}
~Jabber()
{
// p.disconnect();
}
public slots:
void start()
{
//connect( &p, SIGNAL(finished()),
// this, SIGNAL(finished()) );
connect( &p, SIGNAL(msgReceived(QString,QString)),
this, SIGNAL(msgReceived(QString,QString)) );
connect( &p, SIGNAL(peerOnline(QString)),
this, SIGNAL(peerOnline(QString)) );
connect( &p, SIGNAL(peerOffline(QString)),
this, SIGNAL(peerOffline(QString)) );
connect( &p, SIGNAL(connected()),
this, SIGNAL(connected()) );
connect( &p, SIGNAL(disconnected()),
this, SIGNAL(disconnected()) );
connect( &p, SIGNAL(jidChanged(QString)),
this, SIGNAL(jidChanged(QString)) );
connect( &p, SIGNAL(authError(int,const QString&)),
this, SIGNAL(authError(int,const QString&)) );
p.go();
}
void disconnect()
{
QMetaObject::invokeMethod( &p,
"disconnect",
Qt::QueuedConnection
);
}
void sendMsg(const QString& to, const QString& msg)
{
QMetaObject::invokeMethod( &p,
"sendMsg",
Qt::QueuedConnection,
Q_ARG(const QString, to),
Q_ARG(const QString, msg)
);
}
void broadcastMsg(const QString &msg)
{
QMetaObject::invokeMethod( &p,
"broadcastMsg",
Qt::QueuedConnection,
Q_ARG(const QString, msg)
);
}
signals:
//void finished();
void msgReceived(const QString&, const QString&); //from, msg
void peerOnline(const QString&);
void peerOffline(const QString&);
void connected();
void disconnected();
void jidChanged(const QString&);
void authError(int, const QString&);
private:
Jabber_p p;
};
#endif

532
src/jabber/jabber_p.cpp Normal file
View File

@@ -0,0 +1,532 @@
#include "jabber_p.h"
#include <QDebug>
#include <QTime>
using namespace gloox;
using namespace std;
Jabber_p::Jabber_p( const QString& jid, const QString& password, const QString& server, const int port )
: QObject()
{
qDebug() << Q_FUNC_INFO;
qsrand( QTime( 0, 0, 0 ).secsTo( QTime::currentTime() ) );
m_presences[Presence::Available] = "available";
m_presences[Presence::Chat] = "chat";
m_presences[Presence::Away] = "away";
m_presences[Presence::DND] = "dnd";
m_presences[Presence::XA] = "xa";
m_presences[Presence::Unavailable] = "unavailable";
m_presences[Presence::Probe] = "probe";
m_presences[Presence::Error] = "error";
m_presences[Presence::Invalid] = "invalid";
m_jid = JID( jid.toStdString() );
if( m_jid.resource().find( "tomahawk" ) == std::string::npos )
{
qDebug() << "!!! Setting your resource to 'tomahawk' prior to logging in to jabber";
m_jid.setResource( QString( "tomahawk%1" ).arg( qrand() ).toStdString() );
}
qDebug() << "Our JID set to:" << m_jid.full().c_str();
// the google hack, because they filter disco features they don't know.
if( m_jid.server().find( "googlemail." ) != string::npos
|| m_jid.server().find( "gmail." ) != string::npos
|| m_jid.server().find( "gtalk." ) != string::npos )
{
if( m_jid.resource().find( "tomahawk" ) == string::npos )
{
qDebug() << "Forcing your /resource to contain 'tomahawk' (the google workaround)";
m_jid.setResource( "tomahawk-tomahawk" );
}
}
m_client = QSharedPointer<gloox::Client>( new gloox::Client( m_jid, password.toStdString(), port) );
if( !server.isEmpty() )
m_client->setServer( server.toStdString() );
}
Jabber_p::~Jabber_p()
{
// qDebug() << Q_FUNC_INFO;
if( m_client )
{
// m_client->disco()->removeDiscoHandler( this );
m_client->rosterManager()->removeRosterListener();
m_client->removeConnectionListener( this );
}
}
void
Jabber_p::go()
{
m_client->registerConnectionListener( this );
m_client->rosterManager()->registerRosterListener( this );
m_client->logInstance().registerLogHandler( LogLevelWarning, LogAreaAll, this );
m_client->registerMessageHandler( this );
/*
m_client->disco()->registerDiscoHandler( this );
m_client->disco()->setVersion( "gloox_tomahawk", GLOOX_VERSION, "xplatform" );
m_client->disco()->setIdentity( "client", "bot" );
m_client->disco()->addFeature( "tomahawk:player" );
*/
m_client->setPresence( Presence::Available, 1, "Tomahawk available" );
// m_client->connect();
// return;
if( m_client->connect( false ) )
{
emit connected();
m_timer.singleShot(0, this, SLOT(doJabberRecv()));
}
}
void
Jabber_p::doJabberRecv()
{
ConnectionError ce = m_client->recv(100);
if( ce != ConnNoError )
{
qDebug() << "Jabber_p::Recv failed, disconnected";
}
else
{
m_timer.singleShot(100, this, SLOT(doJabberRecv()));
}
}
void
Jabber_p::disconnect()
{
if(m_client)
{
m_client->disconnect();
}
}
void
Jabber_p::sendMsg( const QString& to, const QString& msg )
{
if( QThread::currentThread() != thread() )
{
qDebug() << Q_FUNC_INFO << "invoking in correct thread, not"
<< QThread::currentThread();
QMetaObject::invokeMethod( this, "sendMsg",
Qt::QueuedConnection,
Q_ARG( const QString, to ),
Q_ARG( const QString, msg )
);
return;
}
qDebug() << Q_FUNC_INFO << to << msg;
Message m( Message::Chat, JID(to.toStdString()), msg.toStdString(), "" );
m_client->send( m ); // assuming this is threadsafe
}
void
Jabber_p::broadcastMsg( const QString &msg )
{
if( QThread::currentThread() != thread() )
{
QMetaObject::invokeMethod( this, "broadcastMsg",
Qt::QueuedConnection,
Q_ARG(const QString, msg)
);
return;
}
std::string msg_s = msg.toStdString();
foreach( const QString& jidstr, m_peers.keys() )
{
Message m(Message::Chat, JID(jidstr.toStdString()), msg_s, "");
m_client->send( m );
}
}
/// GLOOX IMPL STUFF FOLLOWS
void
Jabber_p::onConnect()
{
// update jid resource, servers like gtalk use resource binding and may
// have changed our requested /resource
if( m_client->resource() != m_jid.resource() )
{
m_jid.setResource( m_client->resource() );
QString jidstr( m_jid.full().c_str() );
emit jidChanged( jidstr );
}
qDebug() << "Connected as:" << m_jid.full().c_str();
}
void
Jabber_p::onDisconnect( ConnectionError e )
{
qDebug() << "Jabber Disconnected";
QString error;
switch(e)
{
case AuthErrorUndefined:
error = " No error occurred, or error condition is unknown";
break;
case SaslAborted:
error = "The receiving entity acknowledges an &lt;abort/&gt; element sent "
"by the initiating entity; sent in reply to the &lt;abort/&gt; element.";
break;
case SaslIncorrectEncoding:
error = "The data provided by the initiating entity could not be processed "
"because the [BASE64] encoding is incorrect (e.g., because the encoding "
"does not adhere to the definition in Section 3 of [BASE64]); sent in "
"reply to a &lt;response/&gt; element or an &lt;auth/&gt; element with "
"initial response data.";
break;
case SaslInvalidAuthzid:
error = "The authzid provided by the initiating entity is invalid, either "
"because it is incorrectly formatted or because the initiating entity "
"does not have permissions to authorize that ID; sent in reply to a "
"&lt;response/&gt; element or an &lt;auth/&gt; element with initial "
"response data.";
break;
case SaslInvalidMechanism:
error = "The initiating entity did not provide a mechanism or requested a "
"mechanism that is not supported by the receiving entity; sent in reply "
"to an &lt;auth/&gt; element.";
break;
case SaslMalformedRequest:
error = "The request is malformed (e.g., the &lt;auth/&gt; element includes "
"an initial response but the mechanism does not allow that); sent in "
"reply to an &lt;abort/&gt;, &lt;auth/&gt;, &lt;challenge/&gt;, or "
"&lt;response/&gt; element.";
break;
case SaslMechanismTooWeak:
error = "The mechanism requested by the initiating entity is weaker than "
"server policy permits for that initiating entity; sent in reply to a "
"&lt;response/&gt; element or an &lt;auth/&gt; element with initial "
"response data.";
break;
case SaslNotAuthorized:
error = "The authentication failed because the initiating entity did not "
"provide valid credentials (this includes but is not limited to the "
"case of an unknown username); sent in reply to a &lt;response/&gt; "
"element or an &lt;auth/&gt; element with initial response data. ";
break;
case SaslTemporaryAuthFailure:
error = "The authentication failed because of a temporary error condition "
"within the receiving entity; sent in reply to an &lt;auth/&gt; element "
"or &lt;response/&gt; element.";
break;
case NonSaslConflict:
error = "XEP-0078: Resource Conflict";
break;
case NonSaslNotAcceptable:
error = "XEP-0078: Required Information Not Provided";
break;
case NonSaslNotAuthorized:
error = "XEP-0078: Incorrect Credentials";
break;
case ConnAuthenticationFailed:
error = "Authentication failed";
break;
case ConnNoSupportedAuth:
error = "No supported auth mechanism";
break;
default :
error ="UNKNOWN ERROR";
}
qDebug() << "Connection error msg:" << error;
emit authError(e, error);
emit disconnected();
Q_ASSERT(0); //this->exit(1);
}
bool
Jabber_p::onTLSConnect( const CertInfo& info )
{
qDebug() << Q_FUNC_INFO
<< "Status" << info.status
<< "issuer" << info.issuer.c_str()
<< "peer" << info.server.c_str()
<< "proto" << info.protocol.c_str()
<< "mac" << info.mac.c_str()
<< "cipher" << info.cipher.c_str()
<< "compression" << info.compression.c_str()
<< "from" << ctime( (const time_t*)&info.date_from )
<< "to" << ctime( (const time_t*)&info.date_to )
;
//onConnect();
return true;
}
void
Jabber_p::handleMessage( const Message& m, MessageSession * /*session*/ )
{
QString from = QString::fromStdString( m.from().full() );
QString msg = QString::fromStdString( m.body() );
if( msg.length() == 0 ) return;
qDebug() << "Jabber_p::handleMessage" << from << msg;
//printf( "from: %s, type: %d, subject: %s, message: %s, thread id: %s\n",
// msg.from().full().c_str(), msg.subtype(),
// msg.subject().c_str(), msg.body().c_str(), msg.thread().c_str() );
//sendMsg( from, QString("You said %1").arg(msg) );
emit msgReceived( from, msg );
}
void
Jabber_p::handleLog( LogLevel level, LogArea area, const std::string& message )
{
qDebug() << Q_FUNC_INFO
<< "level:" << level
<< "area:" << area
<< "msg:" << message.c_str();
}
/// ROSTER STUFF
// {{{
void
Jabber_p::onResourceBindError( ResourceBindError error )
{
qDebug() << Q_FUNC_INFO;
}
void
Jabber_p::onSessionCreateError( SessionCreateError error )
{
qDebug() << Q_FUNC_INFO;
}
void
Jabber_p::handleItemSubscribed( const JID& jid )
{
qDebug() << Q_FUNC_INFO << jid.full().c_str();
}
void
Jabber_p::handleItemAdded( const JID& jid )
{
qDebug() << Q_FUNC_INFO << jid.full().c_str();
}
void
Jabber_p::handleItemUnsubscribed( const JID& jid )
{
qDebug() << Q_FUNC_INFO << jid.full().c_str();
}
void
Jabber_p::handleItemRemoved( const JID& jid )
{
qDebug() << Q_FUNC_INFO << jid.full().c_str();
}
void
Jabber_p::handleItemUpdated( const JID& jid )
{
qDebug() << Q_FUNC_INFO << jid.full().c_str();
}
// }}}
void
Jabber_p::handleRoster( const Roster& roster )
{
// qDebug() << Q_FUNC_INFO;
Roster::const_iterator it = roster.begin();
for ( ; it != roster.end(); ++it )
{
if ( (*it).second->subscription() != S10nBoth ) continue;
qDebug() << (*it).second->jid().c_str() << (*it).second->name().c_str();
//printf("JID: %s\n", (*it).second->jid().c_str());
}
// mark ourselves as "extended away" lowest priority:
// there is no "invisible" in the spec. XA is the lowest?
//m_client->setPresence( Presence::Available, 1, "Tomahawk App, not human" );
}
void
Jabber_p::handleRosterError( const IQ& /*iq*/ )
{
qDebug() << Q_FUNC_INFO;
}
void
Jabber_p::handleRosterPresence( const RosterItem& item, const std::string& resource,
Presence::PresenceType presence, const std::string& /*msg*/ )
{
JID jid( item.jid() );
jid.setResource( resource );
QString fulljid( jid.full().c_str() );
qDebug() << "* handleRosterPresence" << fulljid << presence;
if( jid == m_jid )
return;
// ignore anyone not running tomahawk:
if( jid.full().find( "/tomahawk" ) == string::npos )
{
// Disco them to check if they are tomahawk-capable
//qDebug() << "No tomahawk resource, DISCOing... " << jid.full().c_str();
//m_client->disco()->getDiscoInfo( jid, "", this, 0 );
return;
}
//qDebug() << Q_FUNC_INFO << "jid: " << QString::fromStdString(item.jid())
// << " resource: " << QString::fromStdString(resource)
// << " presencetype " << presence;
// "going offline" event
if ( !presenceMeansOnline( presence ) &&
( !m_peers.contains( fulljid ) ||
presenceMeansOnline( m_peers.value(fulljid) )
)
)
{
m_peers[ fulljid ] = presence;
qDebug() << "* Peer goes offline:" << fulljid;
emit peerOffline( fulljid );
return;
}
// "coming online " event
if( presenceMeansOnline( presence ) &&
( !m_peers.contains( fulljid ) ||
!presenceMeansOnline( m_peers.value( fulljid ) )
)
)
{
m_peers[ fulljid ] = presence;
qDebug() << "* Peer goes online:" << fulljid;
emit peerOnline( fulljid );
return;
}
//qDebug() << "Updating presence data for" << fulljid;
m_peers[ fulljid ] = presence;
}
void
Jabber_p::handleSelfPresence( const RosterItem& item, const std::string& resource,
Presence::PresenceType presence, const std::string& msg )
{
// qDebug() << Q_FUNC_INFO;
handleRosterPresence( item, resource, presence, msg );
}
bool
Jabber_p::handleSubscriptionRequest( const JID& jid, const std::string& /*msg*/ )
{
qDebug() << Q_FUNC_INFO << jid.bare().c_str();
StringList groups;
m_client->rosterManager()->subscribe( jid, "", groups, "" );
return true;
}
bool
Jabber_p::handleUnsubscriptionRequest( const JID& jid, const std::string& /*msg*/ )
{
qDebug() << Q_FUNC_INFO << jid.bare().c_str();
return true;
}
void
Jabber_p::handleNonrosterPresence( const Presence& presence )
{
qDebug() << Q_FUNC_INFO << presence.from().full().c_str();
}
/// END ROSTER STUFF
/// DISCO STUFF
void
Jabber_p::handleDiscoInfo( const JID& from, const Disco::Info& info, int context)
{
QString jidstr( from.full().c_str() );
//qDebug() << "DISCOinfo" << jidstr;
if ( info.hasFeature("tomahawk:player") )
{
qDebug() << "Peer online and DISCOed ok:" << jidstr;
m_peers.insert( jidstr, Presence::XA );
emit peerOnline( jidstr );
}
else
{
//qDebug() << "Peer DISCO has no tomahawk:" << jidstr;
}
}
void
Jabber_p::handleDiscoItems( const JID& /*iq*/, const Disco::Items&, int /*context*/ )
{
qDebug() << Q_FUNC_INFO;
}
void
Jabber_p::handleDiscoError( const JID& j, const Error* e, int /*context*/ )
{
qDebug() << Q_FUNC_INFO << j.full().c_str() << e->text().c_str() << e->type();
}
/// END DISCO STUFF
bool Jabber_p::presenceMeansOnline( Presence::PresenceType p )
{
switch(p)
{
case Presence::Invalid:
case Presence::Unavailable:
case Presence::Error:
return false;
break;
default:
return true;
}
}

131
src/jabber/jabber_p.h Normal file
View File

@@ -0,0 +1,131 @@
/*
This is the Jabber client that the rest of the app sees
Gloox stuff should NOT leak outside this class.
We may replace gloox later, this interface should remain the same.
*/
#ifndef JABBER_P_H
#define JABBER_P_H
#include <QObject>
#include <QSharedPointer>
#include <QMap>
#include <QThread>
#include <QTimer>
#include <string>
#include <gloox/client.h>
#include <gloox/messagesessionhandler.h>
#include <gloox/messagehandler.h>
#include <gloox/messageeventhandler.h>
#include <gloox/messageeventfilter.h>
#include <gloox/chatstatehandler.h>
#include <gloox/chatstatefilter.h>
#include <gloox/connectionlistener.h>
#include <gloox/disco.h>
#include <gloox/message.h>
#include <gloox/discohandler.h>
#include <gloox/stanza.h>
#include <gloox/gloox.h>
#include <gloox/lastactivity.h>
#include <gloox/loghandler.h>
#include <gloox/logsink.h>
#include <gloox/connectiontcpclient.h>
#include <gloox/connectionsocks5proxy.h>
#include <gloox/connectionhttpproxy.h>
#include <gloox/messagehandler.h>
#include <gloox/rostermanager.h>
#include <gloox/siprofileft.h>
#include <gloox/siprofilefthandler.h>
#include <gloox/bytestreamdatahandler.h>
#include <gloox/error.h>
#include <gloox/presence.h>
#include <gloox/rosteritem.h>
#if defined( WIN32 ) || defined( _WIN32 )
# include <windows.h>
#endif
class Jabber_p :
public QObject,
public gloox::ConnectionListener,
public gloox::RosterListener,
public gloox::MessageHandler,
gloox::LogHandler
//public gloox::DiscoHandler,
{
Q_OBJECT
public:
explicit Jabber_p( const QString& jid, const QString& password, const QString& server = "", const int port = -1 );
virtual ~Jabber_p();
void disconnect();
/// GLOOX IMPLEMENTATION STUFF FOLLOWS
virtual void onConnect();
virtual void onDisconnect( gloox::ConnectionError e );
virtual bool onTLSConnect( const gloox::CertInfo& info );
virtual void handleMessage( const gloox::Message& msg, gloox::MessageSession * /*session*/ );
virtual void handleLog( gloox::LogLevel level, gloox::LogArea area, const std::string& message );
/// ROSTER STUFF
virtual void onResourceBindError( gloox::ResourceBindError error );
virtual void onSessionCreateError( gloox::SessionCreateError error );
virtual void handleItemSubscribed( const gloox::JID& jid );
virtual void handleItemAdded( const gloox::JID& jid );
virtual void handleItemUnsubscribed( const gloox::JID& jid );
virtual void handleItemRemoved( const gloox::JID& jid );
virtual void handleItemUpdated( const gloox::JID& jid );
virtual void handleRoster( const gloox::Roster& roster );
virtual void handleRosterError( const gloox::IQ& /*iq*/ );
virtual void handleRosterPresence( const gloox::RosterItem& item, const std::string& resource,
gloox::Presence::PresenceType presence, const std::string& /*msg*/ );
virtual void handleSelfPresence( const gloox::RosterItem& item, const std::string& resource,
gloox::Presence::PresenceType presence, const std::string& msg );
virtual bool handleSubscriptionRequest( const gloox::JID& jid, const std::string& /*msg*/ );
virtual bool handleUnsubscriptionRequest( const gloox::JID& jid, const std::string& /*msg*/ );
virtual void handleNonrosterPresence( const gloox::Presence& presence );
/// END ROSTER STUFF
/// DISCO STUFF
virtual void handleDiscoInfo( const gloox::JID& from, const gloox::Disco::Info& info, int context);
virtual void handleDiscoItems( const gloox::JID& /*iq*/, const gloox::Disco::Items&, int /*context*/ );
virtual void handleDiscoError( const gloox::JID& /*iq*/, const gloox::Error*, int /*context*/ );
/// END DISCO STUFF
protected:
/////virtual void run();
signals:
void msgReceived( const QString&, const QString& ); //from, msg
void peerOnline( const QString& );
void peerOffline( const QString& );
void connected();
void disconnected();
void jidChanged( const QString& );
void authError( int, const QString& );
public slots:
void go();
void sendMsg( const QString& to, const QString& msg );
void broadcastMsg( const QString &msg );
private slots:
void doJabberRecv();
private:
bool presenceMeansOnline( gloox::Presence::PresenceType p );
QSharedPointer<gloox::Client> m_client;
gloox::JID m_jid;
QMap<gloox::Presence::PresenceType, QString> m_presences;
QMap<QString, gloox::Presence::PresenceType> m_peers;
QTimer m_timer; // for recv()
};
#endif // JABBER_H

View File

@@ -0,0 +1,86 @@
#include "remoteioconnection.h"
#include <QFile>
RemoteIOConnection::RemoteIOConnection(Servent * s, FileTransferSession * fts)
: Connection(s), m_fts(fts)
{
qDebug() << "CTOR " << id() ;
}
RemoteIOConnection::~RemoteIOConnection()
{
qDebug() << "DTOR " << id() ;
}
QString RemoteIOConnection::id() const
{
return QString("RemoteIOConnection[%1]").arg(m_fts->fid());
}
void RemoteIOConnection::shutdown(bool wait)
{
Connection::shutdown(wait);
/*if(!wait)
{
Connection::shutdown(wait);
return;
}
qDebug() << id() << " shutdown requested - waiting until we've received all data TODO";
*/
}
void RemoteIOConnection::setup()
{
if(m_fts->type() == FileTransferSession::RECEIVING)
{
qDebug() << "RemoteIOConnection in RX mode";
return;
}
qDebug() << "RemoteIOConnection in TX mode, fid:" << m_fts->fid();
QString url("/tmp/test.mp3");
qDebug() << "TODO map fid to file://, hardcoded for now";
qDebug() << "Opening for transmission:" << url;
m_readdev = QSharedPointer<QFile>(new QFile(url));
m_readdev->open(QIODevice::ReadOnly);
if(!m_readdev->isOpen())
{
qDebug() << "WARNING file is not readable: " << url;
shutdown();
}
// send chunks within our event loop, since we're not in our own thread
sendSome();
}
void RemoteIOConnection::handleMsg(QByteArray msg)
{
Q_ASSERT(m_fts->type() == FileTransferSession::RECEIVING);
m_fts->iodevice()->addData(msg);
if(msg.length()==0) qDebug() << "Got 0len msg. end?";
}
Connection * RemoteIOConnection::clone()
{
Q_ASSERT(false); return 0;
};
void RemoteIOConnection::sendSome()
{
Q_ASSERT(m_fts->type() == FileTransferSession::SENDING);
if(m_readdev->atEnd())
{
qDebug() << "Sent all. DONE";
shutdown(true);
return;
}
QByteArray ba = m_readdev->read(4096);
//qDebug() << "Sending " << ba.length() << " bytes of audiofile";
sendMsg(ba);
QTimer::singleShot(0, this, SLOT(sendSome()));
}

View File

@@ -0,0 +1,38 @@
#ifndef REMOTEIOCONNECTION_H
#define REMOTEIOCONNECTION_H
#include <QIODevice>
#include <QMutex>
#include <QWaitCondition>
#include <QDebug>
#include <QSharedPointer>
#include "controlconnection.h"
#include "filetransfersession.h"
class RemoteIOConnection : public Connection
{
Q_OBJECT
public:
RemoteIOConnection(Servent * s, FileTransferSession * fts);
~RemoteIOConnection();
QString id() const;
void shutdown(bool wait = false);
void setup();
void handleMsg(QByteArray msg);
Connection * clone();
signals:
private slots:
void sendSome();
private:
FileTransferSession * m_fts;
QSharedPointer<QIODevice> m_readdev;
};
#endif // REMOTEIOCONNECTION_H

101
src/junk/remoteiodevice.cpp Normal file
View File

@@ -0,0 +1,101 @@
#include "remoteiodevice.h"
RemoteIODevice::RemoteIODevice(RemoteIOConnection * c)
: m_eof(false), m_totalAdded(0), m_rioconn(c)
{
qDebug() << "CTOR RemoteIODevice";
}
RemoteIODevice::~RemoteIODevice()
{
qDebug() << "DTOR RemoteIODevice";
m_rioconn->shutdown();
}
void RemoteIODevice::close()
{
qDebug() << "RemoteIODevice::close";
QIODevice::close();
deleteLater();
}
bool RemoteIODevice::open ( OpenMode mode )
{
return QIODevice::open(mode & QIODevice::ReadOnly);
}
qint64 RemoteIODevice::bytesAvailable () const
{
return m_buffer.length();
}
bool RemoteIODevice::isSequential () const
{
return true;
};
bool RemoteIODevice::atEnd() const
{
return m_eof && m_buffer.length() == 0;
};
void RemoteIODevice::addData(QByteArray msg)
{
m_mut_recv.lock();
if(msg.length()==0)
{
m_eof=true;
//qDebug() << "addData finished, entire file received. EOF.";
m_mut_recv.unlock();
m_wait.wakeAll();
return;
}
else
{
m_buffer.append(msg);
m_totalAdded += msg.length();
//qDebug() << "RemoteIODevice has seen in total: " << m_totalAdded ;
m_mut_recv.unlock();
m_wait.wakeAll();
emit readyRead();
return;
}
}
qint64 RemoteIODevice::writeData ( const char * data, qint64 maxSize )
{
Q_ASSERT(false);
return 0;
}
qint64 RemoteIODevice::readData ( char * data, qint64 maxSize )
{
//qDebug() << "RemIO::readData, bytes in buffer: " << m_buffer.length();
m_mut_recv.lock();
if(m_eof && m_buffer.length() == 0)
{
// eof
qDebug() << "readData called when EOF";
m_mut_recv.unlock();
return 0;
}
if(!m_buffer.length())// return 0;
{
//qDebug() << "WARNING readData when buffer is empty";
m_mut_recv.unlock();
return 0;
}
int len;
if(maxSize>=m_buffer.length()) // whole buffer
{
len = m_buffer.length();
memcpy(data, m_buffer.constData(), len);
m_buffer.clear();
} else { // partial
len = maxSize;
memcpy(data, m_buffer.constData(), len);
m_buffer.remove(0,len);
}
m_mut_recv.unlock();
return len;
}

43
src/junk/remoteiodevice.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef REMOTEIODEVICE_H
#define REMOTEIODEVICE_H
#include <QIODevice>
#include <QMutex>
#include <QWaitCondition>
#include <QDebug>
#include <QBuffer>
#include "remoteioconnection.h"
class RemoteIOConnection;
class RemoteIODevice : public QIODevice
{
Q_OBJECT
public:
RemoteIODevice(RemoteIOConnection * c);
~RemoteIODevice();
virtual void close();
virtual bool open ( OpenMode mode );
qint64 bytesAvailable () const;
virtual bool isSequential () const;
virtual bool atEnd() const;
public slots:
void addData(QByteArray msg);
protected:
virtual qint64 writeData ( const char * data, qint64 maxSize );
virtual qint64 readData ( char * data, qint64 maxSize );
private:
QByteArray m_buffer;
QMutex m_mut_wait, m_mut_recv;
QWaitCondition m_wait;
bool m_eof;
int m_totalAdded;
RemoteIOConnection * m_rioconn;
};
#endif // REMOTEIODEVICE_H

7
src/main.cpp Normal file
View File

@@ -0,0 +1,7 @@
#include "tomahawk/tomahawkapp.h"
int main( int argc, char *argv[] )
{
TomahawkApp a( argc, argv );
return a.exec();
}

141
src/msg.h Normal file
View File

@@ -0,0 +1,141 @@
/*
Msg is a wire msg used by p2p connections.
Msgs have a 5-byte header:
- 4 bytes length, big endian
- 1 byte flags
Flags indicate if the payload is compressed/json/etc.
Use static factory method to create, pass around shared pointers: msp_ptr
*/
#ifndef MSG_H
#define MSG_H
#include <QByteArray>
#include <QSharedPointer>
#include <QtEndian>
#include <QIODevice>
#include <QDebug>
#include <qjson/parser.h>
#include <qjson/serializer.h>
#include <qjson/qobjecthelper.h>
class Msg;
typedef QSharedPointer<Msg> msg_ptr;
class Msg
{
friend class MsgProcessor;
public:
enum Flag
{
RAW = 1,
JSON = 2,
FRAGMENT = 4,
COMPRESSED = 8,
DBOP = 16,
UNUSED_FLAG_6 = 32,
UNUSED_FLAG_7 = 64,
SETUP = 128 // used to handshake/auth the connection prior to handing over to Connection subclass
};
virtual ~Msg()
{
//qDebug() << Q_FUNC_INFO;
}
/// constructs new msg you wish to send
static msg_ptr factory( const QByteArray& ba, char f )
{
return msg_ptr( new Msg( ba, f ) );
}
/// constructs an incomplete new msg that is missing the payload data
static msg_ptr begin( char* headerToParse )
{
quint32 lenBE = *( (quint32*) headerToParse );
quint8 flags = *( (quint8*) (headerToParse+4) );
return msg_ptr( new Msg( qFromBigEndian(lenBE), flags ) );
}
/// completes msg construction by providing payload data
void fill( const QByteArray& ba )
{
Q_ASSERT( m_incomplete );
Q_ASSERT( ba.length() == (qint32)m_length );
m_payload = ba;
m_incomplete = false;
}
/// frames the msg and writes to the wire:
bool write( QIODevice * device )
{
quint32 size = qToBigEndian( m_length );
quint8 flags = m_flags;
if( device->write( (const char*) &size, sizeof(quint32) ) != sizeof(quint32) ) return false;
if( device->write( (const char*) &flags, sizeof(quint8) ) != sizeof(quint8) ) return false;
if( device->write( (const char*) m_payload.data(), m_length ) != m_length ) return false;
return true;
}
// len(4) + flags(1)
static quint8 headerSize() { return sizeof(quint32) + sizeof(quint8); }
quint32 length() const { return m_length; }
bool is( Flag flag ) { return m_flags & flag; }
const QByteArray& payload() const
{
Q_ASSERT( m_incomplete == false );
return m_payload;
}
QVariant& json()
{
Q_ASSERT( is(JSON) );
Q_ASSERT( !is(COMPRESSED) );
if( !m_json_parsed )
{
QJson::Parser p;
bool ok;
m_json = p.parse( m_payload, &ok );
m_json_parsed = true;
}
return m_json;
}
char flags() const { return m_flags; }
private:
/// used when constructing Msg you wish to send
Msg( const QByteArray& ba, char f )
: m_payload( ba ),
m_length( ba.length() ),
m_flags( f ),
m_incomplete( false ),
m_json_parsed( false)
{
}
/// used when constructung Msg off the wire:
Msg( quint32 len, quint8 flags )
: m_length( len ),
m_flags( flags ),
m_incomplete( true ),
m_json_parsed( false)
{
}
QByteArray m_payload;
quint32 m_length;
char m_flags;
bool m_incomplete;
QVariant m_json;
bool m_json_parsed;
};
#endif // MSG_H

113
src/msgprocessor.cpp Normal file
View File

@@ -0,0 +1,113 @@
#include "msgprocessor.h"
#include "tomahawk/tomahawkapp.h"
MsgProcessor::MsgProcessor( quint32 mode, quint32 t ) :
QObject(), m_mode( mode ), m_threshold( t ), m_totmsgsize( 0 )
{
moveToThread( APP->servent().thread() );
}
void MsgProcessor::append( msg_ptr msg )
{
if( QThread::currentThread() != thread() )
{
qDebug() << "reinvoking msgprocessor::append in correct thread, ie not" << QThread::currentThread();
QMetaObject::invokeMethod( this, "append", Qt::QueuedConnection, Q_ARG(msg_ptr, msg) );
return;
}
m_msgs.append( msg );
m_msg_ready.insert( msg.data(), false );
m_totmsgsize += msg->payload().length();
if( m_mode & NOTHING )
{
//qDebug() << "MsgProcessor::NOTHING";
handleProcessedMsg( msg );
return;
}
QFuture<msg_ptr> fut = QtConcurrent::run(&MsgProcessor::process, msg, m_mode, m_threshold);
QFutureWatcher<msg_ptr> * watcher = new QFutureWatcher<msg_ptr>;
connect( watcher, SIGNAL( finished() ),
this, SLOT( processed() ),
Qt::QueuedConnection );
watcher->setFuture( fut );
}
void MsgProcessor::processed()
{
QFutureWatcher<msg_ptr> * watcher = (QFutureWatcher<msg_ptr> *) sender();
msg_ptr msg = watcher->result();
watcher->deleteLater();
handleProcessedMsg( msg );
}
void MsgProcessor::handleProcessedMsg( msg_ptr msg )
{
Q_ASSERT( QThread::currentThread() == thread() );
m_msg_ready.insert( msg.data(), true );
while( !m_msgs.isEmpty() )
{
if( m_msg_ready.value( m_msgs.first().data() ) )
{
msg_ptr m = m_msgs.takeFirst();
m_msg_ready.remove( m.data() );
//qDebug() << Q_FUNC_INFO << "totmsgsize:" << m_totmsgsize;
emit ready( m );
}
else
{
return;
}
}
//qDebug() << Q_FUNC_INFO << "EMPTY, no msgs left.";
emit empty();
}
/// This method is run by QtConcurrent:
msg_ptr MsgProcessor::process( msg_ptr msg, quint32 mode, quint32 threshold )
{
// uncompress if needed
if( (mode & UNCOMPRESS_ALL) && msg->is( Msg::COMPRESSED ) )
{
qDebug() << "MsgProcessor::UNCOMPRESSING";
msg->m_payload = qUncompress( msg->payload() );
msg->m_length = msg->m_payload.length();
msg->m_flags ^= Msg::COMPRESSED;
}
// parse json payload into qvariant if needed
if( (mode & PARSE_JSON) &&
msg->is( Msg::JSON ) &&
msg->m_json_parsed == false )
{
qDebug() << "MsgProcessor::PARSING JSON";
bool ok;
QJson::Parser parser;
msg->m_json = parser.parse( msg->payload(), &ok );
msg->m_json_parsed = true;
}
// compress if needed
if( (mode & COMPRESS_IF_LARGE) &&
!msg->is( Msg::COMPRESSED )
&& msg->length() > threshold )
{
qDebug() << "MsgProcessor::COMPRESSING";
msg->m_payload = qCompress( msg->payload(), 9 );
msg->m_length = msg->m_payload.length();
msg->m_flags |= Msg::COMPRESSED;
}
return msg;
}

63
src/msgprocessor.h Normal file
View File

@@ -0,0 +1,63 @@
/*
MsgProcessor is a FIFO queue of msg_ptr, you .add() a msg_ptr, and
it emits done(msg_ptr) for each msg, preserving the order.
It can be configured to auto-compress, or de-compress msgs for sending
or receiving.
It uses QtConcurrent, but preserves msg order.
NOT threadsafe.
*/
#ifndef MSGPROCESSOR_H
#define MSGPROCESSOR_H
#include <QObject>
#include "msg.h"
#include <QtConcurrentRun>
#include <QFuture>
#include <QFutureWatcher>
#include <qjson/parser.h>
#include <qjson/serializer.h>
#include <qjson/qobjecthelper.h>
class MsgProcessor : public QObject
{
Q_OBJECT
public:
enum Mode
{
NOTHING = 0,
COMPRESS_IF_LARGE = 1,
UNCOMPRESS_ALL = 2,
PARSE_JSON = 4
};
explicit MsgProcessor( quint32 mode = NOTHING, quint32 t = 512 );
void setMode( quint32 m ) { m_mode = m ; }
static msg_ptr process( msg_ptr msg, quint32 mode, quint32 threshold );
int length() const { return m_msgs.length(); }
signals:
void ready( msg_ptr );
void empty();
public slots:
void append( msg_ptr msg );
void processed();
private:
void handleProcessedMsg( msg_ptr msg );
quint32 m_mode;
quint32 m_threshold;
QList<msg_ptr> m_msgs;
QMap< Msg*, bool> m_msg_ready;
unsigned int m_totmsgsize;
};
#endif // MSGPROCESSOR_H

210
src/musicscanner.cpp Normal file
View File

@@ -0,0 +1,210 @@
#include "musicscanner.h"
#include "tomahawk/tomahawkapp.h"
#include "database.h"
#include "databasecommand_dirmtimes.h"
#include "databasecommand_addfiles.h"
using namespace Tomahawk;
MusicScanner::MusicScanner( const QString& dir, quint32 bs )
: QThread()
, m_dir( dir )
, m_batchsize( bs )
{
moveToThread( this );
m_ext2mime.insert( "mp3", "audio/mpeg" );
// m_ext2mime.insert( "aac", "audio/mp4" );
// m_ext2mime.insert( "m4a", "audio/mp4" );
// m_ext2mime.insert( "mp4", "audio/mp4" );
// m_ext2mime.insert( "flac", "audio/flac" );
#ifndef NO_OGG
// not compiled on windows yet
m_ext2mime.insert( "ogg", "application/ogg" );
#endif
}
void
MusicScanner::run()
{
QTimer::singleShot( 0, this, SLOT( startScan() ) );
exec();
}
void
MusicScanner::startScan()
{
qDebug() << "Loading mtimes...";
m_scanned = m_skipped = 0;
m_skippedFiles.clear();
// trigger the scan once we've loaded old mtimes for dirs below our path
DatabaseCommand_DirMtimes* cmd = new DatabaseCommand_DirMtimes( m_dir );
connect( cmd, SIGNAL( done( const QMap<QString, unsigned int>& ) ),
SLOT( setMtimes( const QMap<QString, unsigned int>& ) ), Qt::DirectConnection );
connect( cmd, SIGNAL( done( const QMap<QString,unsigned int>& ) ),
SLOT( scan() ), Qt::DirectConnection );
TomahawkApp::instance()->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );
}
void
MusicScanner::setMtimes( const QMap<QString, unsigned int>& m )
{
m_dirmtimes = m;
}
void
MusicScanner::scan()
{
TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( 0 );
qDebug() << "Scanning, num saved mtimes from last scan:" << m_dirmtimes.size();
connect( this, SIGNAL( batchReady( QVariantList ) ),
SLOT( commitBatch( QVariantList ) ), Qt::DirectConnection );
DirLister* lister = new DirLister( QDir( m_dir, 0 ), m_dirmtimes );
connect( lister, SIGNAL( fileToScan( QFileInfo ) ),
SLOT( scanFile( QFileInfo ) ), Qt::QueuedConnection );
// queued, so will only fire after all dirs have been scanned:
connect( lister, SIGNAL( finished( const QMap<QString, unsigned int>& ) ),
SLOT( listerFinished( const QMap<QString, unsigned int>& ) ), Qt::QueuedConnection );
connect( lister, SIGNAL( finished() ), lister, SLOT( deleteLater() ) );
lister->start();
}
void
MusicScanner::listerFinished( const QMap<QString, unsigned int>& newmtimes )
{
qDebug() << Q_FUNC_INFO;
// any remaining stuff that wasnt emitted as a batch:
if( m_scannedfiles.length() )
{
TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( m_scanned );
commitBatch( m_scannedfiles );
}
// save mtimes, then quit thread
DatabaseCommand_DirMtimes* cmd = new DatabaseCommand_DirMtimes( newmtimes );
connect( cmd, SIGNAL( finished() ), SLOT( quit() ) );
TomahawkApp::instance()->database()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );
qDebug() << "Scanning complete, saving to database. "
"(scanned" << m_scanned << "skipped" << m_skipped << ")";
qDebug() << "Skipped the following files (no tags / no valid audio):";
foreach( const QString& s, m_skippedFiles )
qDebug() << s;
}
void
MusicScanner::commitBatch( const QVariantList& tracks )
{
if ( tracks.length() )
{
qDebug() << Q_FUNC_INFO << tracks.length();
source_ptr localsrc = TomahawkApp::instance()->sourcelist().getLocal();
TomahawkApp::instance()->database()->enqueue(
QSharedPointer<DatabaseCommand>( new DatabaseCommand_AddFiles( tracks, localsrc ) )
);
}
}
void
MusicScanner::scanFile( const QFileInfo& fi )
{
QVariant m = readFile( fi );
if( m.toMap().isEmpty() )
return;
m_scannedfiles << m;
if( m_batchsize != 0 &&
(quint32)m_scannedfiles.length() >= m_batchsize )
{
qDebug() << "batchReady, size:" << m_scannedfiles.length();
emit batchReady( m_scannedfiles );
m_scannedfiles.clear();
}
}
QVariant
MusicScanner::readFile( const QFileInfo& fi )
{
if ( ! m_ext2mime.contains( fi.suffix().toLower() ) )
{
m_skipped++;
return QVariantMap(); // invalid extension
}
if( m_scanned % 3 == 0 )
TomahawkApp::instance()->sourcelist().getLocal()->scanningProgress( m_scanned );
if( m_scanned % 100 == 0 )
qDebug() << "SCAN" << m_scanned << fi.absoluteFilePath();
TagLib::FileRef f( fi.absoluteFilePath().toUtf8().constData() );
if ( f.isNull() || !f.tag() )
{
// qDebug() << "Doesn't seem to be a valid audiofile:" << fi.absoluteFilePath();
m_skippedFiles << fi.absoluteFilePath();
m_skipped++;
return QVariantMap();
}
int bitrate = 0;
int duration = 0;
TagLib::Tag *tag = f.tag();
if ( f.audioProperties() )
{
TagLib::AudioProperties *properties = f.audioProperties();
duration = properties->length();
bitrate = properties->bitrate();
}
QString artist = TStringToQString( tag->artist() ).trimmed();
QString album = TStringToQString( tag->album() ).trimmed();
QString track = TStringToQString( tag->title() ).trimmed();
if ( artist.isEmpty() || track.isEmpty() )
{
// FIXME: do some clever filename guessing
// qDebug() << "No tags found, skipping" << fi.absoluteFilePath();
m_skippedFiles << fi.absoluteFilePath();
m_skipped++;
return QVariantMap();
}
QString mimetype = m_ext2mime.value( fi.suffix().toLower() );
QString url( "file://%1" );
QVariantMap m;
m["url"] = url.arg( fi.absoluteFilePath() );
m["lastmodified"] = fi.lastModified().toUTC().toTime_t();
m["size"] = (unsigned int)fi.size();
m["hash"] = ""; // TODO
m["mimetype"] = mimetype;
m["duration"] = duration;
m["bitrate"] = bitrate;
m["artist"] = artist;
m["album"] = album;
m["track"] = track;
m["albumpos"] = tag->track();
m_scanned++;
return m;
}

133
src/musicscanner.h Normal file
View File

@@ -0,0 +1,133 @@
#ifndef MUSICSCANNER_H
#define MUSICSCANNER_H
#include <taglib/fileref.h>
#include <taglib/tag.h>
#include <QVariantMap>
#include <QThread>
#include <QDir>
#include <QFileInfo>
#include <QString>
#include <QDebug>
#include <QDateTime>
class MusicScanner : public QThread
{
Q_OBJECT
public:
MusicScanner( const QString& dir, quint32 bs = 0 );
protected:
void run();
signals:
//void fileScanned( QVariantMap );
void finished( int, int );
void batchReady( const QVariantList& );
private:
QVariant readFile( const QFileInfo& fi );
private slots:
void listerFinished( const QMap<QString, unsigned int>& newmtimes );
void scanFile( const QFileInfo& fi );
void startScan();
void scan();
void setMtimes( const QMap<QString, unsigned int>& m );
void commitBatch( const QVariantList& );
private:
QString m_dir;
QMap<QString,QString> m_ext2mime; // eg: mp3 -> audio/mpeg
unsigned int m_scanned;
unsigned int m_skipped;
QList<QString> m_skippedFiles;
QMap<QString, unsigned int> m_dirmtimes;
QMap<QString, unsigned int> m_newdirmtimes;
QList<QVariant> m_scannedfiles;
quint32 m_batchsize;
};
#include <QTimer>
// descend dir tree comparing dir mtimes to last known mtime
// emit signal for any dir with new content, so we can scan it.
// finally, emit the list of new mtimes we observed.
class DirLister : public QThread
{
Q_OBJECT
public:
DirLister( QDir d, QMap<QString, unsigned int>& mtimes )
: QThread(), m_dir( d ), m_dirmtimes( mtimes )
{
qDebug() << Q_FUNC_INFO;
moveToThread(this);
}
~DirLister()
{
qDebug() << Q_FUNC_INFO;
}
protected:
void run()
{
QTimer::singleShot(0,this,SLOT(go()));
exec();
}
signals:
void fileToScan( QFileInfo );
void finished( const QMap<QString, unsigned int>& );
private slots:
void go()
{
scanDir( m_dir, 0 );
emit finished( m_newdirmtimes );
}
void scanDir( QDir dir, int depth )
{
QFileInfoList dirs;
const uint mtime = QFileInfo( dir.absolutePath() ).lastModified().toUTC().toTime_t();
m_newdirmtimes.insert( dir.absolutePath(), mtime );
if ( m_dirmtimes.contains( dir.absolutePath() ) &&
mtime == m_dirmtimes.value( dir.absolutePath() )
)
{
// dont scan this dir, unchanged since last time.
}
else
{
dir.setFilter( QDir::Files | QDir::Readable | QDir::NoDotAndDotDot );
dir.setSorting( QDir::Name );
dirs = dir.entryInfoList();
foreach( QFileInfo di, dirs )
{
emit fileToScan( di );
}
}
dir.setFilter( QDir::Dirs | QDir::Readable | QDir::NoDotAndDotDot );
dirs = dir.entryInfoList();
foreach( QFileInfo di, dirs )
{
scanDir( di.absoluteFilePath(), depth + 1 );
}
}
private:
QDir m_dir;
QMap<QString, unsigned int> m_dirmtimes;
QMap<QString, unsigned int> m_newdirmtimes;
};
#endif

198
src/pipeline.cpp Normal file
View File

@@ -0,0 +1,198 @@
#include "tomahawk/pipeline.h"
#include <QDebug>
#include <QMutexLocker>
#include "tomahawk/functimeout.h"
#include "tomahawk/tomahawkapp.h"
#include "database/database.h"
using namespace Tomahawk;
Pipeline::Pipeline( QObject* parent )
: QObject( parent )
, m_index_ready( false )
{
}
void
Pipeline::databaseReady()
{
connect( APP->database(), SIGNAL(indexReady()), this, SLOT(indexReady()), Qt::QueuedConnection );
APP->database()->loadIndex();
}
void Pipeline::indexReady()
{
qDebug() << Q_FUNC_INFO << "shuting this many pending queries:" << m_queries_pending.size();
m_index_ready = true;
foreach( const query_ptr& q, m_queries_pending )
{
q->setLastPipelineWeight( 101 );
shunt( q );
}
m_queries_pending.clear();
}
void
Pipeline::removeResolver( Resolver* r )
{
m_resolvers.removeAll( r );
}
void
Pipeline::addResolver( Resolver* r, bool sort )
{
m_resolvers.append( r );
if( sort )
{
qSort( m_resolvers.begin(),
m_resolvers.end(),
Pipeline::resolverSorter );
}
qDebug() << "Adding resolver" << r->name();
/* qDebug() << "Current pipeline:";
foreach( Resolver * r, m_resolvers )
{
qDebug() << "* score:" << r->weight()
<< "pref:" << r->preference()
<< "name:" << r->name();
}*/
}
void
Pipeline::add( const QList<query_ptr>& qlist )
{
{
QMutexLocker lock( &m_mut );
foreach( const query_ptr& q, qlist )
{
qDebug() << Q_FUNC_INFO << (qlonglong)q.data() << q->toString();
if( !m_qids.contains( q->id() ) )
{
m_qids.insert( q->id(), q );
}
}
}
/*
Since resolvers are async, we now dispatch to the highest weighted ones
and after timeout, dispatch to next highest etc, aborting when solved
If index not yet loaded, leave in the pending list instead.
(they are shunted when index is ready)
*/
if( m_index_ready )
{
foreach( const query_ptr& q, qlist )
{
q->setLastPipelineWeight( 101 );
shunt( q ); // bump into next stage of pipeline (highest weights are 100)
}
}
else
{
qDebug() << "Placing query in pending queue - index not ready yet";
m_queries_pending.append( qlist );
}
}
void
Pipeline::add( const query_ptr& q )
{
//qDebug() << Q_FUNC_INFO << (qlonglong)q.data() << q->toString();
QList< query_ptr > qlist;
qlist << q;
add( qlist );
}
void
Pipeline::reportResults( QID qid, const QList< result_ptr >& results )
{
QMutexLocker lock( &m_mut );
if( !m_qids.contains( qid ) )
{
qDebug() << "reportResults called for unknown QID";
return;
}
const query_ptr& q = m_qids.value( qid );
//qDebug() << Q_FUNC_INFO << qid;
//qDebug() << "solved query:" << (qlonglong)q.data() << q->toString();
q->addResults( results );
//qDebug() << "Results for " << q->toString() << ", just added" << results.length();
foreach( const result_ptr& r, q->results() )
{
m_rids.insert( r->id(), r );
//qDebug() << "* " << (results.contains(r) ? "NEW" : "") << r->toString();
}
}
void
Pipeline::shunt( const query_ptr& q )
{
if( q->solved() )
{
qDebug() << "Query solved, pipeline aborted:" << q->toString()
<< "numresults:" << q->results().length();
return;
}
unsigned int lastweight = 0;
unsigned int lasttimeout = 0;
foreach( Resolver* r, m_resolvers )
{
if ( r->weight() >= q->lastPipelineWeight() )
continue;
if ( lastweight == 0 )
{
lastweight = r->weight();
lasttimeout = r->timeout();
//qDebug() << "Shunting into weight" << lastweight << "q:" << q->toString();
}
if ( lastweight == r->weight() )
{
// snag the lowest timeout at this weight
if ( r->timeout() < lasttimeout )
lasttimeout = r->timeout();
// resolvers aren't allowed to block in this call:
//qDebug() << "Dispaching to resolver" << r->name();
r->resolve( q->toVariant() );
}
else
break;
}
if ( lastweight > 0 )
{
q->setLastPipelineWeight( lastweight );
//qDebug() << "Shunting in" << lasttimeout << "ms, q:" << q->toString();
new FuncTimeout( lasttimeout, boost::bind( &Pipeline::shunt, this, q ) );
}
else
{
//qDebug() << "Reached end of pipeline for:" << q->toString();
// reached end of pipeline
}
}
bool
Pipeline::resolverSorter( const Resolver* left, const Resolver* right )
{
if( left->weight() == right->weight() )
return left->preference() > right->preference();
else
return left->weight() > right->weight();
}

Some files were not shown because too many files have changed in this diff Show More